From f78c3be4db18b826cbc50640b84e97c6bd72a4ea Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 19 May 2026 15:36:01 -0400 Subject: [PATCH 01/40] initial commit for verify --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 7f0e592c1..c64caa156 100644 --- a/readme.md +++ b/readme.md @@ -27,6 +27,7 @@ Our Docusaurus website will help you get started in running and contributing to ## Developer usage + - Full local dev (builds, starts the portless HTTPS proxy, starts Azurite, and runs the app-level dev servers): ```bash From 09e9ab0a295574df2ae1dca1209839f730fb9f35 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 19 May 2026 15:57:41 -0400 Subject: [PATCH 02/40] staging changes for portless modification - using worktree as an env variable and building them into the url for our various portless hosted services --- .snyk | 15 + apps/api/package.json | 7 +- apps/api/start-azurite.mjs | 57 + apps/api/start-dev.mjs | 17 +- apps/api/turbo.json | 5 + apps/docs/package.json | 1 + apps/docs/turbo.json | 6 + apps/server-mongodb-memory-mock/package.json | 3 +- .../start-mongo.mjs | 35 + apps/server-mongodb-memory-mock/turbo.json | 6 + apps/server-oauth2-mock/package.json | 1 + .../src/portal-discovery.ts | 9 +- apps/server-oauth2-mock/start-dev.mjs | 24 + apps/server-oauth2-mock/turbo.json | 6 + apps/ui-community/mock-oidc.users.json | 11 + apps/ui-community/package.json | 1 + apps/ui-community/start-dev.mjs | 24 + apps/ui-community/turbo.json | 6 + apps/ui-staff/package.json | 1 + apps/ui-staff/start-dev.mjs | 23 + build-pipeline/scripts/portless-hostnames.mjs | 88 ++ build-pipeline/scripts/worktree-ports.mjs | 100 ++ knip.json | 5 +- package.json | 4 +- .../server-oauth2-mock-seedwork/package.json | 2 +- .../ocom-verification/e2e-tests/cucumber.js | 2 +- .../ocom-verification/e2e-tests/package.json | 4 +- .../src/shared/support/oauth2-login.ts | 14 +- .../src/shared/support/servers/index.ts | 12 +- .../shared/support/servers/portless-server.ts | 115 +- .../shared/support/servers/test-api-server.ts | 49 +- .../servers/test-community-vite-server.ts | 40 + .../support/servers/test-environment.ts | 30 +- .../support/servers/test-oauth2-server.ts | 74 +- .../support/servers/test-vite-server.ts | 41 - .../shared/support/shared-infrastructure.ts | 47 +- .../verification-shared/src/servers/index.ts | 1 + .../src/servers/test-server.interface.ts | 40 + .../verification-shared/src/settings/index.ts | 2 + .../src/settings/local-settings.ts | 37 +- .../src/settings/portless-settings.ts | 65 ++ .../src/settings/timeout-settings.ts | 48 + packages/ocom/service-otel/package.json | 2 +- pnpm-lock.yaml | 1034 ++++++++++------- pnpm-workspace.yaml | 9 +- turbo.json | 7 + 46 files changed, 1543 insertions(+), 587 deletions(-) create mode 100644 apps/api/start-azurite.mjs create mode 100644 apps/server-mongodb-memory-mock/start-mongo.mjs create mode 100644 apps/server-oauth2-mock/start-dev.mjs create mode 100644 apps/ui-community/start-dev.mjs create mode 100644 apps/ui-staff/start-dev.mjs create mode 100644 build-pipeline/scripts/portless-hostnames.mjs create mode 100644 build-pipeline/scripts/worktree-ports.mjs create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts create mode 100644 packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts create mode 100644 packages/ocom-verification/verification-shared/src/settings/portless-settings.ts create mode 100644 packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts diff --git a/.snyk b/.snyk index 97bb87848..02a4e4b14 100644 --- a/.snyk +++ b/.snyk @@ -76,3 +76,18 @@ ignore: reason: 'Transitive dependency in Docusaurus; not exploitable in current usage.' expires: '2026-06-28T00:00:00.000Z' created: '2026-05-11T10:00:00.000Z' + 'SNYK-JS-AI-16734889': + - '* > ai@5.0.105': + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' + 'SNYK-JS-AISDKPROVIDERUTILS-16734888': + - '* > @ai-sdk/provider-utils@3.0.18': + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' + 'SNYK-JS-AISDKPROVIDERUTILS-16735288': + - '* > @ai-sdk/provider-utils@3.0.18': + reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' + expires: '2026-06-18T00:00:00.000Z' + created: '2026-05-18T11:04:00.000Z' diff --git a/apps/api/package.json b/apps/api/package.json index fce77a1f4..6041983b2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,6 +10,8 @@ "build": "tsgo --build && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs", + "predev:portless": "pnpm run prepare:deploy && pnpm run sync-local-settings", + "dev:portless": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", @@ -22,7 +24,7 @@ "prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings", "start": "func start --typescript --script-root deploy/", "sync-local-settings": "node -e \"const fs=require('node:fs'); fs.mkdirSync('deploy',{recursive:true}); if (fs.existsSync('local.settings.json')) fs.copyFileSync('local.settings.json','deploy/local.settings.json');\"", - "azurite": "azurite-blob --silent --location ../../__blobstorage__ & azurite-queue --silent --location ../../__queuestorage__ & azurite-table --silent --location ../../__tablestorage__" + "azurite": "node start-azurite.mjs" }, "dependencies": { "@azure/functions": "catalog:", @@ -31,8 +33,8 @@ "@ocom/application-services": "workspace:*", "@ocom/context-spec": "workspace:*", "@ocom/event-handler": "workspace:*", - "@ocom/graphql-handler": "workspace:*", "@ocom/graphql": "workspace:*", + "@ocom/graphql-handler": "workspace:*", "@ocom/persistence": "workspace:*", "@ocom/rest": "workspace:*", "@ocom/service-apollo-server": "workspace:*", @@ -47,6 +49,7 @@ "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@vitest/coverage-istanbul": "catalog:", + "azurite": "^3.35.0", "rimraf": "catalog:", "rolldown": "1.0.0-beta.55", "typescript": "catalog:", diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs new file mode 100644 index 000000000..ac3dff4e4 --- /dev/null +++ b/apps/api/start-azurite.mjs @@ -0,0 +1,57 @@ +import { spawn } from 'node:child_process'; +import net from 'node:net'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { getAzuritePorts } from '../../build-pipeline/scripts/worktree-ports.mjs'; + +const ports = getAzuritePorts(); +const worktreeName = process.env.WORKTREE_NAME ?? ''; +const storageSuffix = worktreeName ? `-${worktreeName}` : ''; + +function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +if (await isPortListening(ports.blob)) { + console.log(`[azurite] already running (blob port ${ports.blob}), skipping`); + process.exit(0); +} + +const blobDir = `../../__blobstorage__${storageSuffix}`; +const queueDir = `../../__queuestorage__${storageSuffix}`; +const tableDir = `../../__tablestorage__${storageSuffix}`; + +const procs = [ + spawn('azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir], { stdio: 'inherit' }), + spawn('azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir], { stdio: 'inherit' }), + spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), +]; + +let exited = 0; +for (const proc of procs) { + proc.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + if (++exited === procs.length) process.exit(0); + return; + } + console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); + for (const p of procs) p.kill(); + process.exit(code ?? 1); + }); +} + +process.on('SIGINT', () => { + for (const p of procs) p.kill('SIGINT'); +}); +process.on('SIGTERM', () => { + for (const p of procs) p.kill('SIGTERM'); +}); diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 7c705d8c7..978b8744b 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -2,6 +2,8 @@ import { spawn } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { getAzuriteConnectionString, getMongoConnectionString } from '../../build-pipeline/scripts/worktree-ports.mjs'; const envPort = process.env.PORT; @@ -18,7 +20,20 @@ const childEnv = { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), }; -const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort], { +// Only inject worktree-scoped overrides when running in worktree mode. +// When WORKTREE_NAME is absent, local.settings.json remains the source of truth. +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community'); + childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); + childEnv.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); + childEnv.AZURE_STORAGE_CONNECTION_STRING = getAzuriteConnectionString(); + childEnv.AzureWebJobsStorage = getAzuriteConnectionString(); + // Disable the Node.js inspector — port 5858 is already used by the primary worktree. + childEnv.languageWorkers__node__arguments = ''; +} + +const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { stdio: 'inherit', env: childEnv, }); diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 25ef0ca81..0f908f3c8 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -11,6 +11,11 @@ "dependsOn": ["build"], "interruptible": true, "inputs": [] + }, + "dev:portless": { + "dependsOn": ["build"], + "interruptible": true, + "inputs": [] } } } diff --git a/apps/docs/package.json b/apps/docs/package.json index 64b40e9f8..10766a652 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs", + "dev:portless": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "docusaurus start --port 3001", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index 94418f57b..d39e3fdc8 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -7,6 +7,12 @@ "interruptible": false, "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, + "dev:portless": { + "dependsOn": [], + "persistent": true, + "interruptible": false, + "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] + }, "test": { "inputs": ["$TURBO_EXTENDS$", "!docs/**", "!blog/**", "!static/**"] }, diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index d34485afc..2372d4635 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -11,7 +11,8 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "dev": "tsx src/index.ts", + "dev:portless": "node start-mongo.mjs" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs new file mode 100644 index 000000000..1b39ae1ef --- /dev/null +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -0,0 +1,35 @@ +import net from 'node:net'; +import { getMongoPort } from '../../build-pipeline/scripts/worktree-ports.mjs'; + +const MONGO_PORT = getMongoPort(); + +function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +if (await isPortListening(MONGO_PORT)) { + console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); + process.exit(0); +} + +// Not running — start it via tsx with the worktree-scoped port +const { + default: { spawn }, +} = await import('node:child_process'); +const child = spawn('tsx', ['src/index.ts'], { + stdio: 'inherit', + env: { ...process.env, PORT: String(MONGO_PORT) }, +}); +child.on('exit', (code) => { + process.exit(code ?? 1); +}); diff --git a/apps/server-mongodb-memory-mock/turbo.json b/apps/server-mongodb-memory-mock/turbo.json index 01cb45a99..b0f2f18a9 100644 --- a/apps/server-mongodb-memory-mock/turbo.json +++ b/apps/server-mongodb-memory-mock/turbo.json @@ -7,6 +7,12 @@ "persistent": true, "interruptible": true, "inputs": [] + }, + "dev:portless": { + "dependsOn": ["build"], + "persistent": true, + "interruptible": true, + "inputs": [] } } } diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index c371c0518..bc92782ff 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -12,6 +12,7 @@ "format:check": "biome format .", "start": "node dist/index.js", "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts", + "dev:portless": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest" diff --git a/apps/server-oauth2-mock/src/portal-discovery.ts b/apps/server-oauth2-mock/src/portal-discovery.ts index 43b3329c5..34c63cc87 100644 --- a/apps/server-oauth2-mock/src/portal-discovery.ts +++ b/apps/server-oauth2-mock/src/portal-discovery.ts @@ -119,16 +119,17 @@ function buildPortalFromConfig(config: MockOidcConfig, parsedEnv: Record { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/apps/server-oauth2-mock/turbo.json b/apps/server-oauth2-mock/turbo.json index d2d188a5b..325758390 100644 --- a/apps/server-oauth2-mock/turbo.json +++ b/apps/server-oauth2-mock/turbo.json @@ -9,6 +9,12 @@ "interruptible": true, "persistent": true, "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"] + }, + "dev:portless": { + "dependsOn": ["build"], + "interruptible": true, + "persistent": true, + "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json", "start-dev.mjs"] } } } diff --git a/apps/ui-community/mock-oidc.users.json b/apps/ui-community/mock-oidc.users.json index a9953d8c4..b37ea3537 100644 --- a/apps/ui-community/mock-oidc.users.json +++ b/apps/ui-community/mock-oidc.users.json @@ -20,5 +20,16 @@ "family_name": "Doe", "tid": "test-tenant-id" } + }, + { + "username": "owner@test.example", + "sub": "aaaaaaaa-bbbb-1ccc-9ddd-eeeeeeeeee01", + "password": "password", + "claims": { + "email": "owner@test.example", + "given_name": "Test", + "family_name": "Owner", + "tid": "test-tenant-id" + } } ] diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 5e8926d5d..86bc14f1d 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -10,6 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless ownercommunity.localhost --force vite", + "dev:portless": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs new file mode 100644 index 000000000..a13661535 --- /dev/null +++ b/apps/ui-community/start-dev.mjs @@ -0,0 +1,24 @@ +import { spawn } from 'node:child_process'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; + +const hostnames = getHostnames(); + +const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { + stdio: 'inherit', + env: { + ...process.env, + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/community'), + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), + VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), + VITE_APP_UI_COMMUNITY_BASE_URL: buildPortlessUrl(hostnames.uiCommunity), + }, +}); + +child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index d5f21a945..e253b58b7 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -6,6 +6,12 @@ "persistent": true, "interruptible": false, "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + }, + "dev:portless": { + "dependsOn": ["^build"], + "persistent": true, + "interruptible": false, + "inputs": [".env", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] } } } diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index 1b07d4527..bd35eaf13 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -10,6 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", + "dev:portless": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs new file mode 100644 index 000000000..4b656ce70 --- /dev/null +++ b/apps/ui-staff/start-dev.mjs @@ -0,0 +1,23 @@ +import { spawn } from 'node:child_process'; +import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; + +const hostnames = getHostnames(); + +const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { + stdio: 'inherit', + env: { + ...process.env, + VITE_APP_UI_STAFF_AAD_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/staff'), + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), + VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), + }, +}); + +child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; +}); diff --git a/build-pipeline/scripts/portless-hostnames.mjs b/build-pipeline/scripts/portless-hostnames.mjs new file mode 100644 index 000000000..f5df73258 --- /dev/null +++ b/build-pipeline/scripts/portless-hostnames.mjs @@ -0,0 +1,88 @@ +/** + * Shared portless hostname computation for git worktree isolation. + * + * Hostnames are derived from the tracked .env files, so this module contains + * no hardcoded service names. When `WORKTREE_NAME` is set the worktree suffix + * is spliced in before `.localhost`, giving each worktree its own subdomain + * on the shared proxy port. + * + * Default (no worktree): + * ownercommunity.localhost (read from VITE_APP_UI_COMMUNITY_BASE_URL) + * data-access.ownercommunity.localhost (read from VITE_COMMON_API_ENDPOINT) + * + * With WORKTREE_NAME=feature-a: + * ownercommunity.feature-a.localhost + * data-access.ownercommunity.feature-a.localhost + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PORTLESS_PORT = 1355; +const scriptDir = fileURLToPath(new URL('.', import.meta.url)); +const workspaceRoot = resolve(scriptDir, '../..'); + +function readDotEnv(filePath) { + if (!existsSync(filePath)) return {}; + const result = {}; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + return result; +} + +function hostnameFrom(url) { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +/** Splice `.` in before `.localhost` in an existing hostname. */ +function applyWorktreeSuffix(hostname, worktreeName) { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} + +/** + * Returns all service hostnames scoped to the current worktree (if any). + * Hostname shapes are read from the tracked .env files — no names are + * hardcoded in this module. + */ +export function getHostnames() { + const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); + const staffEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-staff/.env')); + const wt = process.env.WORKTREE_NAME ?? ''; + + const uiCommunity = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); + const api = hostnameFrom(uiEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); + const mockAuth = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); + const uiStaff = hostnameFrom(staffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + + if (!uiCommunity || !api || !mockAuth || !uiStaff) { + throw new Error('portless-hostnames: could not derive all hostnames from .env files. ' + 'Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); + } + + return { + uiCommunity: applyWorktreeSuffix(uiCommunity, wt), + uiStaff: applyWorktreeSuffix(uiStaff, wt), + api: applyWorktreeSuffix(api, wt), + mockAuth: applyWorktreeSuffix(mockAuth, wt), + docs: applyWorktreeSuffix(`docs.${uiCommunity}`, wt), + }; +} + +/** + * Builds a full portless-proxied URL for the given hostname and optional path. + */ +export function buildPortlessUrl(hostname, path = '') { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +export { PORTLESS_PORT }; diff --git a/build-pipeline/scripts/worktree-ports.mjs b/build-pipeline/scripts/worktree-ports.mjs new file mode 100644 index 000000000..9a86b183c --- /dev/null +++ b/build-pipeline/scripts/worktree-ports.mjs @@ -0,0 +1,100 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Worktree-scoped port computation for service isolation. + * + * When WORKTREE_NAME is set, each worktree gets a deterministic port offset + * so MongoDB and Azurite instances don't collide between worktrees. + * + * Default worktree (no WORKTREE_NAME): uses base ports (50000, 10000–10002). + * Named worktree: base + deterministic offset derived from the name's hash. + * + * Collision safety: the unset case always returns 0, and any named worktree + * always returns ≥ 100, so the default worktree can never collide with a + * named one. With 49 buckets the chance of two *named* worktrees colliding + * is ~2% per pair — acceptable for the typical 1–3 concurrent worktrees. + */ + +const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const apiLocalSettingsPaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; +let apiLocalSettingsValues; + +function getSetting(name) { + return process.env[name] ?? getApiLocalSetting(name); +} + +function getApiLocalSetting(name) { + apiLocalSettingsValues ??= readApiLocalSettingsValues(); + return apiLocalSettingsValues[name]; +} + +function readApiLocalSettingsValues() { + for (const settingsPath of apiLocalSettingsPaths) { + if (!fs.existsSync(settingsPath)) continue; + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + return settings.Values ?? {}; + } + return {}; +} + +/** + * Returns a deterministic port offset in the range [100, 4900] (step 100) + * for the current worktree. Returns 0 when WORKTREE_NAME is not set. + */ +export function getWorktreePortOffset() { + const name = process.env.WORKTREE_NAME; + if (!name) return 0; + let hash = 0; + for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; + return ((Math.abs(hash) % 49) + 1) * 100; +} + +/** MongoDB port for the current worktree. */ +export function getMongoPort() { + return 50000 + getWorktreePortOffset(); +} + +/** Azurite blob/queue/table ports for the current worktree. */ +export function getAzuritePorts() { + const offset = getWorktreePortOffset(); + return { + blob: 10000 + offset, + queue: 10001 + offset, + table: 10002 + offset, + }; +} + +/** + * Azurite connection string for worktree-specific ports. + * Returns `UseDevelopmentStorage=true` for the default worktree (port 10000). + */ +export function getAzuriteConnectionString() { + const ports = getAzuritePorts(); + if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; + const accountName = getSetting('STORAGE_ACCOUNT_NAME'); + const accountKey = getSetting('STORAGE_ACCOUNT_KEY'); + if (!accountName || !accountKey) { + throw new Error('[worktree-ports] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); + } + return [ + 'DefaultEndpointsProtocol=http', + `AccountName=${accountName}`, + `AccountKey=${accountKey}`, + `BlobEndpoint=http://127.0.0.1:${ports.blob}/${accountName}`, + `QueueEndpoint=http://127.0.0.1:${ports.queue}/${accountName}`, + `TableEndpoint=http://127.0.0.1:${ports.table}/${accountName}`, + ].join(';'); +} + +/** + * MongoDB connection string with the worktree-specific port patched in. + * Reads COSMOSDB_CONNECTION_STRING from env or local.settings.json and replaces + * the host:port segment. + */ +export function getMongoConnectionString() { + const base = getSetting('COSMOSDB_CONNECTION_STRING'); + if (!base) throw new Error('[worktree-ports] COSMOSDB_CONNECTION_STRING must be set'); + return base.replace(/127\.0\.0\.1:\d+/, `127.0.0.1:${getMongoPort()}`); +} diff --git a/knip.json b/knip.json index 035e1f57f..4a560cc9a 100644 --- a/knip.json +++ b/knip.json @@ -2,8 +2,9 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "workspaces": { "apps/api": { - "entry": ["src/index.ts"], - "project": ["src/**/*.ts"] + "entry": ["src/index.ts", "start-*.mjs"], + "project": ["src/**/*.ts", "*.mjs"], + "ignoreDependencies": ["azurite"] }, "apps/ui-community": { "entry": ["src/main.tsx"], diff --git a/package.json b/package.json index 12aea8a26..c1757bc2c 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,11 @@ "test": "turbo run test", "lint": "turbo run lint", "dev": "pnpm proxy:stop && pnpm proxy:start && turbo watch azurite dev --filter='./apps/*' --filter='./packages/*'", + "dev:portless": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:portless --filter='./apps/*' --filter='./packages/*'", "start": "turbo run build && concurrently --kill-others-on-fail \"pnpm run start:api\" \"pnpm run start:ui-community\"", "proxy:stop": "pnpm exec portless proxy stop || true", "proxy:start": "pnpm exec portless proxy start --https -p 1355", + "proxy:ensure": "pnpm exec portless proxy start --https -p 1355 || true", "format": "turbo run format", "format:check": "turbo run format:check", "format:staged": "biome check --write --staged --no-errors-on-unmatched", @@ -31,6 +33,7 @@ "test:coverage": "turbo run test:coverage", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:portless": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", @@ -77,7 +80,6 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "@vitest/coverage-istanbul": "catalog:", - "azurite": "^3.35.0", "chrome-devtools-mcp": "^0.21.0", "concurrently": "^9.1.2", "husky": "^9.1.7", diff --git a/packages/cellix/server-oauth2-mock-seedwork/package.json b/packages/cellix/server-oauth2-mock-seedwork/package.json index ef0015814..68da0838b 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/package.json +++ b/packages/cellix/server-oauth2-mock-seedwork/package.json @@ -18,7 +18,7 @@ "test:watch": "vitest" }, "dependencies": { - "express": "^4.22.0", + "express": "^4.22.2", "express-rate-limit": "^8.5.1", "jose": "^5.9.6" }, diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index 339a66b11..51912f713 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -7,5 +7,5 @@ export default { formatOptions: { snippetInterface: 'async-await', }, - parallel: 1, + parallel: 0, }; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index cbe99de57..6d3508bc6 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,7 +5,9 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "test:e2e": "pnpm run proxy:start && pnpm run test:e2e:run", + "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", + "proxy:start": "pnpm exec portless proxy start -p 1355", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index b5db83c66..abc2b4638 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -15,8 +15,8 @@ const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url. * The app uses RequireAuth + react-oidc-context. When an unauthenticated * user hits a protected route, RequireAuth calls `signinRedirect()` which * navigates to the mock OAuth2 server's `/authorize` endpoint. The mock - * server auto-completes the flow (no login form) and redirects back with a - * code that the OIDC library exchanges for tokens. + * server redirects to `/login` (since userStore is configured). This + * function fills in the test user credentials and submits the form. */ export async function performOAuth2Login(page: Page): Promise { // Navigate to a protected route to trigger the OIDC signinRedirect flow. @@ -29,6 +29,16 @@ export async function performOAuth2Login(page: Page): Promise { // Navigation may be interrupted by OIDC redirect — this is expected } + // Wait for redirects to settle on either the login page or the app + await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); + + // If the mock OAuth2 login form is shown, fill credentials and submit + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', 'test@example.com'); + await page.fill('input[name="password"]', 'password'); + await page.click('button[type="submit"]'); + } + // Wait for the redirect chain to settle on an authenticated page await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); await page.waitForLoadState('networkidle'); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index 6f2cca847..c096f6636 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -1,6 +1,14 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; -export { buildUrl, cleanupTestEnvironment, initTestEnvironment, setMongoConnectionString } from './test-environment.ts'; +export { TestCommunityViteServer } from './test-community-vite-server.ts'; +export { + buildUrl, + cleanupTestEnvironment, + initTestEnvironment, + mockOidcAudience, + mockOidcEndpoint, + mockOidcIssuer, + setMongoConnectionString, +} from './test-environment.ts'; export { TestOAuth2Server } from './test-oauth2-server.ts'; -export { TestViteServer } from './test-vite-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 8b78c1c02..aea6c3ab1 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,37 +1,69 @@ import { type ChildProcess, spawn } from 'node:child_process'; +import type { TestServer } from '@ocom-verification/verification-shared/servers'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; /** * Abstract base class for portless-proxied servers. * Subclasses define the hostname, command, ready marker, and working directory. * The base class handles spawning via portless, readiness detection, and shutdown. + * + * This implements the TestServer interface for consistency with + * GraphQLTestServer (in-process), while providing subprocess isolation + * for full system tests. */ -export abstract class PortlessServer { +export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; - protected abstract get startupTimeoutMs(): number; protected abstract get spawnArgs(): string[]; protected abstract get cwd(): string; + + protected get executable(): string { + return getPortlessPath(); + } + + protected get probeRequestInit(): RequestInit { + return {}; + } + protected get extraEnv(): Record { return {}; } + protected isProbeHealthy(response: Response): boolean | Promise { + return response.ok; + } + + protected get startupTimeoutMs(): number { + return getTimeout('serverStartup'); + } + + abstract getUrl(): string; + + /** + * Check if server is already running (via health probe). + */ async isAlreadyRunning(): Promise { try { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3_000); - const res = await fetch(this.probeUrl, { signal: controller.signal }); + const probeTimeout = getTimeout('healthProbe'); + const timeout = setTimeout(() => controller.abort(), probeTimeout); + const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); clearTimeout(timeout); - return res.ok; + return await this.isProbeHealthy(res); } catch { return false; } } + /** + * Start the server subprocess and wait for it to be ready. + */ async start(): Promise { if (this.process || this.startedByUs) return; if (await this.isAlreadyRunning()) return; @@ -43,9 +75,10 @@ export abstract class PortlessServer { // Remove NODE_OPTIONS from child process to avoid tsx import issues delete env['NODE_OPTIONS']; - this.process = spawn(getPortlessPath(), this.spawnArgs, { + this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, env, + detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; @@ -53,6 +86,9 @@ export abstract class PortlessServer { await this.waitForReady(); } + /** + * Stop the server gracefully, with fallback to SIGKILL. + */ async stop(): Promise { if (!this.process || !this.startedByUs) return; @@ -60,13 +96,17 @@ export abstract class PortlessServer { this.process = null; this.startedByUs = false; - proc.kill('SIGTERM'); + // SIGINT lets portless run its cleanup branch — deregister the hostname from + // ~/.portless/routes.json before exiting. Fall back to SIGKILL after the + // shutdown timeout for anything that ignores SIGINT. + this.killProcess(proc, 'SIGINT'); + const shutdownTimeout = getTimeout('serverShutdown'); await new Promise((resolve) => { const timeout = setTimeout(() => { - proc.kill('SIGKILL'); + this.killProcess(proc, 'SIGKILL'); resolve(); - }, 10_000); + }, shutdownTimeout); proc.on('exit', () => { clearTimeout(timeout); @@ -87,16 +127,34 @@ export abstract class PortlessServer { return; } + const startupTimeout = this.startupTimeoutMs; const timeout = setTimeout(() => { - reject(new Error(`${this.serverName} did not start within ${this.startupTimeoutMs}ms`)); - }, this.startupTimeoutMs); + reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); + }, startupTimeout); let stderrOutput = ''; - + let ready = false; + + const resolveWhenReachable = () => { + if (ready) return; + ready = true; + + this.waitForProbeReady() + .then(() => { + clearTimeout(timeout); + resolve(); + }) + .catch((error: unknown) => { + clearTimeout(timeout); + reject(error); + }); + }; + + // stdout listener detects the readyMarker then waits for the probe to respond proc.stdout?.on('data', (data: Buffer) => { - if (data.toString().includes(this.readyMarker)) { - clearTimeout(timeout); - resolve(); + const text = data.toString(); + if (text.includes(this.readyMarker)) { + resolveWhenReachable(); } }); @@ -104,19 +162,36 @@ export abstract class PortlessServer { stderrOutput += data.toString(); }); - proc.on('error', (err) => { + proc.on('error', (err: Error) => { clearTimeout(timeout); - this.process = null; - this.startedByUs = false; reject(new Error(`${this.serverName} failed to start: ${err.message}`)); }); - proc.on('exit', (code) => { + proc.on('exit', (code, signal) => { clearTimeout(timeout); this.process = null; this.startedByUs = false; - reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}). stderr: ${stderrOutput.slice(-2000)}`)); + reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); }); }); } + + private async waitForProbeReady(): Promise { + const probeInterval = getTimeout('healthProbeInterval'); + while (!(await this.isAlreadyRunning())) { + await new Promise((resolve) => setTimeout(resolve, probeInterval)); + } + } + + private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { + if (this.useDetachedProcessGroup && proc.pid) { + try { + process.kill(-proc.pid, signal); + return; + } catch { + /* Fall back to killing the direct child below. */ + } + } + proc.kill(signal); + } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 84a87174a..380249d50 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,7 +1,9 @@ import { execFileSync } from 'node:child_process'; import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getMongoConnectionString } from './test-environment.ts'; +import { buildUrl, getHostnames, getMongoConnectionString, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; + +const hostnames = getHostnames(); export class TestApiServer extends PortlessServer { override async start(): Promise { @@ -10,7 +12,7 @@ export class TestApiServer extends PortlessServer { const env = { ...process.env, }; - delete env.NODE_OPTIONS; + delete env['NODE_OPTIONS']; execFileSync('pnpm', ['run', 'predev'], { cwd: this.cwd, @@ -22,19 +24,35 @@ export class TestApiServer extends PortlessServer { } protected get probeUrl() { - return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + return buildUrl(hostnames.api, '/api/graphql'); + } + + protected override get probeRequestInit(): RequestInit { + return { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: '{ __typename }' }), + }; } + + protected override async isProbeHealthy(response: Response): Promise { + if (!response.ok) return false; + try { + const data = (await response.json()) as { data?: { __typename?: string } }; + return data?.data?.__typename === 'Query'; + } catch { + return false; + } + } + protected get readyMarker() { return 'Functions:'; } protected get serverName() { return 'TestApiServer'; } - protected get startupTimeoutMs() { - return 120_000; - } protected get spawnArgs() { - return ['data-access.ownercommunity.localhost', 'node', 'start-dev.mjs']; + return [hostnames.api, 'node', 'start-dev.mjs']; } protected get cwd() { return apiSettings.apiDir; @@ -42,15 +60,24 @@ export class TestApiServer extends PortlessServer { protected override get extraEnv() { return { + NODE_ENV: 'development', languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - ACCOUNT_PORTAL_OIDC_ISSUER: apiSettings.accountPortalOidcIssuer, - ACCOUNT_PORTAL_OIDC_ENDPOINT: apiSettings.accountPortalOidcEndpoint, - VITE_COMMON_API_ENDPOINT: buildUrl('data-access.ownercommunity.localhost', '/api/graphql'), + COSMOSDB_DBNAME: apiSettings.cosmosDbName, + AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', + ACCOUNT_PORTAL_OIDC_ISSUER: mockOidcIssuer, + ACCOUNT_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + ACCOUNT_PORTAL_OIDC_AUDIENCE: mockOidcAudience, + ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', + STAFF_PORTAL_OIDC_ISSUER: mockOidcIssuer, + STAFF_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, + STAFF_PORTAL_OIDC_AUDIENCE: mockOidcAudience, + STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', + VITE_COMMON_API_ENDPOINT: buildUrl(hostnames.api, '/api/graphql'), }; } getUrl(): string { - return buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); + return buildUrl(hostnames.api, '/api/graphql'); } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts new file mode 100644 index 000000000..8de00bfca --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -0,0 +1,40 @@ +import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { PortlessServer } from './portless-server.ts'; +import { buildUrl, getHostnames } from './test-environment.ts'; + +const hostnames = getHostnames(); + +/** + * Starts the community portal Vite dev server via portless. + * + * The `apps/ui-community/start-dev.mjs` script sets all VITE_* environment + * variables at runtime using portless-hostnames.mjs, so no extraEnv overrides + * are needed here beyond suppressing browser auto-launch. + */ +export class TestCommunityViteServer extends PortlessServer { + protected get probeUrl() { + return buildUrl(hostnames.uiCommunity); + } + protected get readyMarker() { + return 'ready in'; + } + protected get serverName() { + return 'TestCommunityViteServer'; + } + protected get spawnArgs() { + return [hostnames.uiCommunity, 'node', 'start-dev.mjs']; + } + protected get cwd() { + return apiSettings.uiCommunityDir; + } + protected override get extraEnv() { + return { + BROWSER: 'none', + NODE_ENV: 'development', + }; + } + + getUrl(): string { + return buildUrl(hostnames.uiCommunity); + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 76c9a1d8d..4a8fbb54f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,30 +1,48 @@ import { execFileSync } from 'node:child_process'; +import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; +/** Module-level hostnames derived from .env files (matches dev:portless pattern). */ +const hostnames = getHostnames(); + +/** OIDC issuer URL for the community portal on the mock auth server. */ +export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community'); + +/** JWKS endpoint used as the OIDC discovery / probe URL. */ +export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; + +/** Audience claim expected in JWTs issued by the mock OIDC server. */ +export const mockOidcAudience = 'mock-client'; + +/** + * Prune orphaned portless route locks from previous test runs. + * The proxy itself is started by the `test:e2e` npm script before the + * Cucumber process spawns, so we only need to clean stale locks here. + */ export function initTestEnvironment() { if (proxyInitialized) return; - execFileSync(getPortlessPath(), ['proxy', 'start', '-p', '1355'], { - timeout: 15_000, + execFileSync(getPortlessPath(), ['prune'], { + timeout: 10_000, stdio: 'pipe', }); proxyInitialized = true; } -export function buildUrl(hostname: string, path = ''): string { - return `https://${hostname}:1355${path}`; -} +export { buildPortlessUrl as buildUrl, getHostnames }; export function setMongoConnectionString(connStr: string): void { mongoConnectionString = connStr; } export function getMongoConnectionString(): string { - if (!mongoConnectionString) throw new Error('MongoDB connection string not set. Start MongoDBTestServer first.'); + if (!mongoConnectionString) { + throw new Error('MongoDB connection string not set — call setMongoConnectionString() first'); + } return mongoConnectionString; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 319f80830..7d843f446 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,81 +1,33 @@ import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; +import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +const hostnames = getHostnames(); + +/** + * Starts the mock OAuth2/OIDC server via portless. + * + * Login is performed by the browser context in oauth2-login.ts rather than + * by programmatic token generation — this tests the real OIDC redirect flow. + */ export class TestOAuth2Server extends PortlessServer { protected get probeUrl() { - return apiSettings.accountPortalOidcEndpoint; + return mockOidcEndpoint; } protected get readyMarker() { - return 'Mock OAuth2 server running'; + return 'Registered OIDC config'; } protected get serverName() { return 'TestOAuth2Server'; } - protected get startupTimeoutMs() { - return 30_000; - } protected get spawnArgs() { - return ['mock-auth.ownercommunity.localhost', 'pnpm', 'exec', 'tsx', 'src/index.ts']; + return [hostnames.mockAuth, 'node', 'start-dev.mjs']; } protected get cwd() { return apiSettings.oauth2MockDir; } - private readonly testUser: { - email: string; - given_name: string; - family_name: string; - }; - - constructor(options?: { - testUser?: { - email?: string; - given_name?: string; - family_name?: string; - }; - }) { - super(); - this.testUser = { - email: options?.testUser?.email ?? 'alice@test.cellix.local', - given_name: options?.testUser?.given_name ?? 'Alice', - family_name: options?.testUser?.family_name ?? 'Test', - }; - } - - protected override get extraEnv() { - return { - EMAIL: this.testUser.email, - GIVEN_NAME: this.testUser.given_name, - FAMILY_NAME: this.testUser.family_name, - BASE_URL: buildUrl('mock-auth.ownercommunity.localhost'), - ALLOWED_REDIRECT_URI: buildUrl('ownercommunity.localhost', '/auth-redirect'), - CLIENT_ID: apiSettings.accountPortalOidcAudience, - }; - } - getUrl(): string { - return apiSettings.accountPortalOidcIssuer; - } - - async generateAccessToken(_audience = 'mock-client'): Promise { - const issuer = this.getUrl(); - const uiBaseUrl = buildUrl('ownercommunity.localhost'); - const redirectUri = `${uiBaseUrl}/auth-redirect`; - - const code = `mock-auth-code-${Buffer.from(redirectUri).toString('base64')}`; - - const response = await fetch(`${issuer}/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ code, grant_type: 'authorization_code' }), - }); - - if (!response.ok) { - throw new Error(`Token request failed: ${response.status} ${await response.text()}`); - } - - const data = (await response.json()) as { access_token: string }; - return data.access_token; + return mockOidcIssuer; } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts deleted file mode 100644 index 44ede2444..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-vite-server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { PortlessServer } from './portless-server.ts'; -import { buildUrl } from './test-environment.ts'; - -export class TestViteServer extends PortlessServer { - protected get probeUrl() { - return buildUrl('ownercommunity.localhost'); - } - protected get readyMarker() { - return 'ready in'; - } - protected get serverName() { - return 'TestViteServer'; - } - protected get startupTimeoutMs() { - return 60_000; - } - protected get spawnArgs() { - return ['ownercommunity.localhost', 'pnpm', 'exec', 'vite']; - } - protected get cwd() { - return apiSettings.uiDir; - } - - protected override get extraEnv() { - const uiBase = buildUrl('ownercommunity.localhost'); - const apiEndpoint = buildUrl('data-access.ownercommunity.localhost', '/api/graphql'); - - return { - BROWSER: 'none', - VITE_APP_UI_COMMUNITY_BASE_URL: uiBase, - VITE_AAD_B2C_ACCOUNT_AUTHORITY: apiSettings.accountPortalOidcIssuer, - VITE_AAD_B2C_REDIRECT_URI: `${uiBase}/auth-redirect`, - VITE_COMMON_API_ENDPOINT: apiEndpoint, - }; - } - - getUrl(): string { - return buildUrl('ownercommunity.localhost'); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index d3d2c3ece..e91cca9b9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,29 +1,41 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { actors } from '@ocom-verification/verification-shared/test-data'; import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestOAuth2Server, TestViteServer } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; -let viteServer: TestViteServer | undefined; +let viteServer: TestCommunityViteServer | undefined; let apiUrl: string | undefined; -let accessToken: string | undefined; let browser: Browser | undefined; let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; let browseTheWeb: BrowseTheWeb | undefined; +let shutdownHandlersRegistered = false; + +function registerShutdownHandlers(): void { + if (shutdownHandlersRegistered) return; + shutdownHandlersRegistered = true; + + const shutdown = (signal: string) => { + void stopAll().finally(() => { + process.exit(signal === 'SIGINT' ? 130 : 143); + }); + }; + + process.once('SIGINT', () => shutdown('SIGINT')); + process.once('SIGTERM', () => shutdown('SIGTERM')); +} + export interface InfrastructureState { apiUrl: string | undefined; - accessToken: string | undefined; browseTheWeb: BrowseTheWeb | undefined; } export function getState(): InfrastructureState { - return { apiUrl, accessToken, browseTheWeb }; + return { apiUrl, browseTheWeb }; } export async function stopAll(): Promise { @@ -56,22 +68,16 @@ export async function stopAll(): Promise { } apiUrl = undefined; browserBaseUrl = undefined; - accessToken = undefined; cleanupTestEnvironment(); } export async function ensureE2EServers(): Promise { + registerShutdownHandlers(); initTestEnvironment(); // Phase 1: Start MongoDB and OAuth2 in parallel (no interdependency) mongoDBServer ??= new MongoDBTestServer(); - oauth2Server ??= new TestOAuth2Server({ - testUser: { - email: actors.CommunityOwner.email, - given_name: actors.CommunityOwner.givenName, - family_name: actors.CommunityOwner.familyName, - }, - }); + oauth2Server ??= new TestOAuth2Server(); const mongo = mongoDBServer; const oauth2 = oauth2Server; const phase1: Promise[] = []; @@ -83,9 +89,9 @@ export async function ensureE2EServers(): Promise { } if (phase1.length > 0) await Promise.all(phase1); - // Phase 2: Start API (needs MongoDB conn string), Vite (independent), and generate token (needs OAuth2) in parallel + // Phase 2: Start API (needs MongoDB conn string) and Vite (independent) in parallel apiServer ??= new TestApiServer(); - viteServer ??= new TestViteServer(); + viteServer ??= new TestCommunityViteServer(); const api = apiServer; const vite = viteServer; const phase2: Promise[] = []; @@ -99,13 +105,6 @@ export async function ensureE2EServers(): Promise { if (!vite.isRunning()) { phase2.push(vite.start()); } - if (!accessToken) { - phase2.push( - oauth2.generateAccessToken(apiSettings.accountPortalOidcAudience).then((token) => { - accessToken = token; - }), - ); - } if (phase2.length > 0) await Promise.all(phase2); browserBaseUrl = viteServer.getUrl(); diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts index 585f779f2..32810914a 100644 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ b/packages/ocom-verification/verification-shared/src/servers/index.ts @@ -7,3 +7,4 @@ export { MongoDBTestServer, seedOwnerCommunityReferenceData, } from './test-mongodb-server.ts'; +export type { TestServer, TestServerOptions } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts new file mode 100644 index 000000000..8b08f6b92 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts @@ -0,0 +1,40 @@ +/** + * Common interface for all test servers (in-process and subprocess). + * + * This abstraction allows acceptance-api and e2e tests to use + * consistent server lifecycle management patterns while choosing + * the appropriate implementation: + * + * - **In-process** (GraphQLTestServer): Fast, isolated, mocked services + * Best for: API acceptance tests, unit-like integration tests + * + * - **Subprocess** (PortlessServer): Full stack, realistic, real services + * Best for: E2E tests, full system integration tests + */ +export interface TestServer { + /** Start the server and return when ready */ + start(): Promise; + + /** Stop the server gracefully */ + stop(): Promise; + + /** Check if server is currently running */ + isRunning(): boolean; + + /** Get the server URL (throws if not running) */ + getUrl(): string; +} + +/** + * Configuration options for test server startup. + */ +export interface TestServerOptions { + /** Port to listen on (0 for random available port) */ + port?: number; + + /** Additional environment variables for subprocess servers */ + env?: Record; + + /** Timeout for server startup (defaults to centralized config) */ + startupTimeoutMs?: number; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts index 88ed046dd..68ac69337 100644 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ b/packages/ocom-verification/verification-shared/src/settings/index.ts @@ -1,4 +1,5 @@ export { apiSettings, uiSettings } from './local-settings.ts'; +export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from './portless-settings.ts'; export { findWorkspaceRoot, readDotEnv, @@ -7,3 +8,4 @@ export { requireSetting, resolveWorkspacePath, } from './settings-utils.ts'; +export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index e1a03e801..c28d7a6f5 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,21 +8,40 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); +/** + * Defaults for E2E/acceptance test settings when local.settings.json is absent + * (e.g. CI pipelines). All values are non-secret mock/localhost references used + * exclusively by the test harness — no real credentials are involved. + */ +const ciDefaults = { + COSMOSDB_CONNECTION_STRING: '', + COSMOSDB_DBNAME: 'owner-community', + COSMOSDB_PORT: '50000', + NODE_ENV: 'development', + ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + ACCOUNT_PORTAL_OIDC_ENDPOINT: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', +} as const; + +function setting(key: keyof typeof ciDefaults): string { + return readSetting(apiValues, key, ciDefaults[key]) ?? ciDefaults[key]; +} + export const apiSettings = { - nodeEnv: readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development', - isDevelopment: (readSetting(apiValues, 'NODE_ENV', 'development') ?? 'development') === 'development', + nodeEnv: setting('NODE_ENV'), + isDevelopment: setting('NODE_ENV') === 'development', - cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING') ?? '', - cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', - cosmosDbPort: Number(readSetting(apiValues, 'COSMOSDB_PORT', '50000')), + cosmosDbConnectionString: setting('COSMOSDB_CONNECTION_STRING'), + cosmosDbName: setting('COSMOSDB_DBNAME'), + cosmosDbPort: Number(setting('COSMOSDB_PORT')), - accountPortalOidcIssuer: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ISSUER') ?? '', - accountPortalOidcEndpoint: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_ENDPOINT') ?? '', - accountPortalOidcAudience: readSetting(apiValues, 'ACCOUNT_PORTAL_OIDC_AUDIENCE', 'mock-client') ?? '', + accountPortalOidcIssuer: setting('ACCOUNT_PORTAL_OIDC_ISSUER'), + accountPortalOidcEndpoint: setting('ACCOUNT_PORTAL_OIDC_ENDPOINT'), + accountPortalOidcAudience: setting('ACCOUNT_PORTAL_OIDC_AUDIENCE'), apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), - uiDir: path.dirname(uiEnvPath), + uiCommunityDir: path.dirname(uiEnvPath), } as const; export const uiSettings = { diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts new file mode 100644 index 000000000..e084702e0 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -0,0 +1,65 @@ +/** + * Portless hostname derivation for test environments. + * + * Mirrors the logic of `build-pipeline/scripts/portless-hostnames.mjs` — + * hostnames are derived from the tracked .env files rather than hardcoded, + * and the `WORKTREE_NAME` suffix is applied automatically. + * + * This keeps test-environment code in sync with the dev:portless startup + * scripts without adding a cross-layer import to build-pipeline/. + */ + +import { findWorkspaceRoot, readDotEnv, resolveWorkspacePath } from './settings-utils.ts'; + +const PORTLESS_PORT = 1355; +const workspaceRoot = findWorkspaceRoot(); + +const uiCommunityEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env')); +const uiStaffEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-staff/.env')); + +function hostnameFromUrl(url: string): string { + try { + return new URL(url).hostname; + } catch { + return ''; + } +} + +/** Splice `.` in before `.localhost` for worktree isolation. */ +function applyWorktreeSuffix(hostname: string): string { + const wt = process.env['WORKTREE_NAME']; + if (!wt) return hostname; + return hostname.replace('.localhost', `.${wt}.localhost`); +} + +/** + * Returns all portless service hostnames scoped to the current worktree. + * Hostnames are derived from the tracked .env files — no names are hardcoded. + */ +export function getHostnames() { + const uiCommunity = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); + const api = hostnameFromUrl(uiCommunityEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); + const mockAuth = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); + const uiStaff = hostnameFromUrl(uiStaffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + + if (!uiCommunity || !api || !mockAuth) { + throw new Error( + 'portless-settings: could not derive hostnames from .env files. ' + 'Ensure apps/ui-community/.env is present with VITE_APP_UI_COMMUNITY_BASE_URL, ' + 'VITE_COMMON_API_ENDPOINT, and VITE_APP_UI_COMMUNITY_B2C_AUTHORITY.', + ); + } + + return { + uiCommunity: applyWorktreeSuffix(uiCommunity), + uiStaff: uiStaff ? applyWorktreeSuffix(uiStaff) : '', + api: applyWorktreeSuffix(api), + mockAuth: applyWorktreeSuffix(mockAuth), + docs: applyWorktreeSuffix(`docs.${uiCommunity}`), + }; +} + +/** Build a full portless HTTPS URL from a hostname and optional path. */ +export function buildPortlessUrl(hostname: string, path = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +export { PORTLESS_PORT }; diff --git a/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts new file mode 100644 index 000000000..309ef7f3e --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts @@ -0,0 +1,48 @@ +/** + * Centralized timeout configuration for all verification test packages. + * + * These timeouts are intentionally generous to accommodate: + * - CI environments with limited resources + * - First-time server startup (cold starts) + * - Parallel test execution contention + */ +export const timeouts = { + /** Default scenario timeout (2 minutes) */ + scenario: 120_000, + + /** Server startup timeout (2 minutes) */ + serverStartup: 120_000, + + /** Server shutdown graceful period (10 seconds) */ + serverShutdown: 10_000, + + /** Health probe timeout (3 seconds) */ + healthProbe: 3_000, + + /** Health probe retry interval (500ms) */ + healthProbeInterval: 500, + + /** UI initialization timeout (30 seconds) */ + uiInit: 30_000, + + /** UI cleanup timeout (10 seconds) */ + uiCleanup: 10_000, +} as const; + +/** Type for timeout configuration keys */ +export type TimeoutKey = keyof typeof timeouts; + +/** + * Get timeout value with optional override from environment. + * Usage: TIMEOUT_SERVER_STARTUP=300000 npm test + */ +export function getTimeout(key: TimeoutKey): number { + const envOverride = process.env[`TIMEOUT_${key.toUpperCase()}`]; + if (envOverride) { + const parsed = Number.parseInt(envOverride, 10); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + return timeouts[key]; +} diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index 18fb61910..0fe4c8bd5 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -35,7 +35,7 @@ "@opentelemetry/instrumentation-mongoose": "0.47.0", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-node": "0.57.2", + "@opentelemetry/sdk-node": "0.217.0", "@opentelemetry/sdk-trace-node": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0464b416e..a16e5e4be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,7 +105,8 @@ overrides: rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 brace-expansion@1.1.12: 1.1.13 - brace-expansion@5.0.4: 5.0.5 + brace-expansion@5.0.4: 5.0.6 + brace-expansion@5.0.5: 5.0.6 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -116,7 +117,7 @@ overrides: svgo: ^3.3.3 yaml@2.8.2: 2.8.3 yauzl@3.2.0: 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 ajv@^6: 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -126,7 +127,9 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 + '@azure/ms-rest-js>uuid': ^3.4.0 + azurite>uuid: ^3.4.0 + ws@8.20.0: 8.20.1 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 @@ -190,9 +193,6 @@ importers: '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) - azurite: - specifier: ^3.35.0 - version: 3.35.0 chrome-devtools-mcp: specifier: ^0.21.0 version: 0.21.0 @@ -290,6 +290,9 @@ importers: '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) + azurite: + specifier: ^3.35.0 + version: 3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -430,7 +433,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -454,7 +457,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -527,7 +530,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -545,7 +548,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -581,7 +584,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -894,11 +897,11 @@ importers: packages/cellix/server-oauth2-mock-seedwork: dependencies: express: - specifier: ^4.22.0 - version: 4.22.1 + specifier: ^4.22.2 + version: 4.22.2 express-rate-limit: specifier: 8.5.1 - version: 8.5.1(express@4.22.1) + version: 8.5.1(express@4.22.2) jose: specifier: ^5.9.6 version: 5.10.0 @@ -1698,8 +1701,8 @@ importers: specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: 0.57.2 - version: 0.57.2(@opentelemetry/api@1.9.0) + specifier: 0.217.0 + version: 0.217.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) @@ -1764,7 +1767,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -1855,7 +1858,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2010,7 +2013,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2092,7 +2095,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2171,7 +2174,7 @@ importers: version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) typescript: specifier: 'catalog:' version: 6.0.3 @@ -2745,8 +2748,8 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@azure-rest/core-client@2.5.1': - resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + '@azure-rest/core-client@2.6.0': + resolution: {integrity: sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==} engines: {node: '>=20.0.0'} '@azure/abort-controller@1.1.0': @@ -2769,9 +2772,12 @@ packages: resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} engines: {node: '>=20.0.0'} - '@azure/core-http-compat@2.3.1': - resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 '@azure/core-lro@2.7.2': resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} @@ -2815,9 +2821,9 @@ packages: resolution: {integrity: sha512-0q5DL4uyR0EZ4RXQKD8MadGH6zTIcloUoS/RVbCpNpej4pwte0xpqYxk8K97Py2RiuUvI7F4GXpoT4046VfufA==} engines: {node: '>=14.0.0'} - '@azure/keyvault-common@2.0.0': - resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} - engines: {node: '>=18.0.0'} + '@azure/keyvault-common@2.1.0': + resolution: {integrity: sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==} + engines: {node: '>=20.0.0'} '@azure/keyvault-keys@4.10.0': resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} @@ -2846,9 +2852,9 @@ packages: resolution: {integrity: sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==} engines: {node: '>=16'} - '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': - resolution: {integrity: sha512-gNCFokEoQQEkhu2T8i1i+1iW2o9wODn2slu5tpqJmjV1W7qf9dxVv6GNXW1P1WC8wMga8BCc2t/oMhOK3iwRQg==} - engines: {node: '>=18.0.0'} + '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0': + resolution: {integrity: sha512-Y8rZOIMXQY/GwNRL+uLVuwIn9aEa/KnnggyYUmFxC1MigmRJCNH5NxMmxKSpddXF9SW6Z1ijRd6Pptd2A5OhGw==} + engines: {node: '>=20.0.0'} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -4806,8 +4812,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@js-joda/core@5.6.5': - resolution: {integrity: sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==} + '@js-joda/core@5.7.0': + resolution: {integrity: sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==} '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} @@ -4902,6 +4908,14 @@ packages: resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.211.0': + resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.217.0': + resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -4914,12 +4928,24 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/configuration@0.217.0': + resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks@1.30.1': resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@1.25.1': resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} engines: {node: '>=14'} @@ -4944,69 +4970,69 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': - resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': + resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.57.2': - resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-http@0.217.0': + resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.57.2': - resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-logs-otlp-proto@0.217.0': + resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': - resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': + resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.57.2': - resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-http@0.217.0': + resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': - resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': + resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.57.2': - resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} - engines: {node: '>=14'} + '@opentelemetry/exporter-prometheus@0.217.0': + resolution: {integrity: sha512-U9MCXxJu0sBCh5aEkylYRR4xVIL8D1CW6dGwvYXbfFr0qveSorfD0XJchCAWoW6QfAAIcY/yxjf4Dj8OgkHBPw==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': - resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': + resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.57.2': - resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-http@0.217.0': + resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.57.2': - resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} - engines: {node: '>=14'} + '@opentelemetry/exporter-trace-otlp-proto@0.217.0': + resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@1.30.1': - resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} - engines: {node: '>=14'} + '@opentelemetry/exporter-zipkin@2.7.1': + resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -5040,33 +5066,39 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.52.1': - resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} - engines: {node: '>=14'} + '@opentelemetry/instrumentation@0.211.0': + resolution: {integrity: sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.57.2': - resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} - engines: {node: '>=14'} + '@opentelemetry/instrumentation@0.217.0': + resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.57.2': - resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.57.2': - resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} - engines: {node: '>=14'} + '@opentelemetry/otlp-exporter-base@0.217.0': + resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.57.2': - resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} - engines: {node: '>=14'} + '@opentelemetry/otlp-grpc-exporter-base@0.217.0': + resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.217.0': + resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5076,24 +5108,42 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-b3@2.7.1': + resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@1.30.1': resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@2.7.1': + resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resources@1.30.1': resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@2.2.0': - resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.217.0': + resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.57.2': resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} engines: {node: '>=14'} @@ -5106,9 +5156,15 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-node@0.57.2': - resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} - engines: {node: '>=14'} + '@opentelemetry/sdk-metrics@2.7.1': + resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.217.0': + resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5118,8 +5174,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.2.0': - resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5130,8 +5186,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.2.0': - resolution: {integrity: sha512-x/LHsDBO3kfqaFx5qSzBljJ5QHsRXrvS4MybBDy1k7Svidb8ZyIPudWVzj3s5LpPkYZIgi9e+7tdsNCnptoelw==} + '@opentelemetry/sdk-trace-node@2.7.1': + resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-web@2.7.1': + resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -6562,8 +6624,8 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@types/readable-stream@4.0.22': - resolution: {integrity: sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==} + '@types/readable-stream@4.0.23': + resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -7012,7 +7074,7 @@ packages: peerDependencies: '@apollo/client': '>=3' graphql: '>=0.11' - qs: ^6.14.2 + qs: 6.15.2 applicationinsights@2.9.8: resolution: {integrity: sha512-eB/EtAXJ6mDLLvHrtZj/7h31qUfnC2Npr2pHGqds5+1OP7BFLsn5us+HCkwTj7Q+1sHXujLphE5Cyvq5grtV6g==} @@ -7227,13 +7289,17 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bl@6.1.5: - resolution: {integrity: sha512-XylDt2P3JBttAwLpORq/hOEX9eJzP0r6Voa46C/WVvad8D1J0jW5876txB8FnzKtbdnU6X4Y1vOEvC6PllJrDg==} + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -7262,8 +7328,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -7349,6 +7415,10 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -7480,6 +7550,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -8113,8 +8186,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dottie@2.0.6: - resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==} + dottie@2.0.7: + resolution: {integrity: sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. dset@3.1.4: @@ -8198,8 +8271,8 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-abstract@1.24.0: - resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-aggregate-error@1.0.14: @@ -8380,6 +8453,10 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -8765,7 +8842,7 @@ packages: crossws: ~0.3 graphql: ^15.10.1 || ^16 uWebSockets.js: ^20 - ws: ^8 + ws: 8.20.1 peerDependenciesMeta: '@fastify/websocket': optional: true @@ -8996,6 +9073,10 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + icss-utils@5.1.0: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -9041,6 +9122,13 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -9378,7 +9466,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: '*' + ws: 8.20.1 istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -9491,21 +9579,15 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - - jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} @@ -9776,10 +9858,6 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.3: - resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} - engines: {node: 20 || >=22} - lru-cache@11.3.5: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} @@ -9787,12 +9865,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - lru.min@1.1.3: - resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} luxon@3.7.2: @@ -10268,16 +10342,18 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - mysql2@3.15.3: - resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + mysql2@3.22.3: + resolution: {integrity: sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==} engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - named-placeholders@1.1.3: - resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} - engines: {node: '>=12.0.0'} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -10665,8 +10741,8 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.13.0: + resolution: {integrity: sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -11246,8 +11322,8 @@ packages: resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} engines: {node: '>=16.0.0'} - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -11269,6 +11345,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -11547,6 +11627,10 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + require-like@0.1.2: resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} @@ -11664,8 +11748,8 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.1.2: @@ -11774,15 +11858,12 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - sequelize-pool@7.1.0: resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==} engines: {node: '>= 10.0.0'} - sequelize@6.37.7: - resolution: {integrity: sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==} + sequelize@6.37.8: + resolution: {integrity: sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==} engines: {node: '>=10.0.0'} peerDependencies: ibm_db: '*' @@ -12031,9 +12112,9 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} + sql-escaper@1.3.3: + resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} srcset@4.0.0: resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} @@ -12741,8 +12822,27 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-to-istanbul@9.3.0: @@ -12756,8 +12856,8 @@ packages: resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} engines: {node: ^20.17.0 || >=22.9.0} - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} value-equal@1.0.1: @@ -13024,8 +13124,8 @@ packages: resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} engines: {node: '>= 12.0.0'} - winston@3.18.3: - resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==} + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} engines: {node: '>= 12.0.0'} wkx@0.5.0: @@ -13061,8 +13161,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13456,7 +13556,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -13473,7 +13573,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -13522,9 +13622,9 @@ snapshots: finalhandler: 2.1.1 graphql: 16.12.0 loglevel: 1.9.2 - lru-cache: 11.3.3 + lru-cache: 11.3.5 negotiator: 1.0.0 - uuid: 14.0.0 + uuid: 11.1.1 whatwg-mimetype: 4.0.0 transitivePeerDependencies: - supports-color @@ -13606,7 +13706,7 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@azure-rest/core-client@2.5.1': + '@azure-rest/core-client@2.6.0': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 @@ -13653,13 +13753,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/core-http-compat@2.3.1': + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2)': dependencies: '@azure/abort-controller': 2.1.2 '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 - transitivePeerDependencies: - - supports-color '@azure/core-lro@2.7.2': dependencies: @@ -13738,18 +13836,18 @@ snapshots: '@azure/msal-browser': 3.30.0 '@azure/msal-node': 2.16.3 events: 3.3.0 - jws: 4.0.0 + jws: 4.0.1 open: 8.4.2 stoppable: 1.1.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/keyvault-common@2.0.0': + '@azure/keyvault-common@2.1.0': dependencies: + '@azure-rest/core-client': 2.6.0 '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 - '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 @@ -13758,21 +13856,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/keyvault-keys@4.10.0': + '@azure/keyvault-keys@4.10.0(@azure/core-client@1.10.1)': dependencies: - '@azure-rest/core-client': 2.5.1 + '@azure-rest/core-client': 2.6.0 '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.10.1 - '@azure/core-http-compat': 2.3.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.22.2) '@azure/core-lro': 2.7.2 '@azure/core-paging': 1.6.2 '@azure/core-rest-pipeline': 1.22.2 '@azure/core-tracing': 1.3.1 '@azure/core-util': 1.13.1 - '@azure/keyvault-common': 2.0.0 + '@azure/keyvault-common': 2.1.0 '@azure/logger': 1.3.0 tslib: 2.8.1 transitivePeerDependencies: + - '@azure/core-client' - supports-color '@azure/logger@1.3.0': @@ -13807,7 +13906,7 @@ snapshots: tough-cookie: 2.5.0 tslib: 1.14.1 tunnel: 0.0.6 - uuid: 14.0.0 + uuid: 3.4.0 xml2js: 0.4.23 transitivePeerDependencies: - debug @@ -13822,17 +13921,17 @@ snapshots: '@azure/msal-node@2.16.3': dependencies: '@azure/msal-common': 14.16.1 - jsonwebtoken: 9.0.2 - uuid: 14.0.0 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 - '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': + '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0': dependencies: '@azure/core-tracing': 1.3.1 '@azure/logger': 1.3.0 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-web': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.211.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -15085,14 +15184,14 @@ snapshots: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 10.0.0 '@cucumber/messages@27.2.0': dependencies: '@types/uuid': 10.0.0 class-transformer: 0.5.1 reflect-metadata: 0.2.2 - uuid: 14.0.0 + uuid: 11.0.5 '@cucumber/messages@32.2.0': dependencies: @@ -16405,10 +16504,10 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@whatwg-node/disposablestack': 0.0.6 graphql: 16.12.0 - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.0) - isomorphic-ws: 5.0.0(ws@8.20.0) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - bufferutil @@ -16436,9 +16535,9 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@types/ws': 8.18.1 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16610,10 +16709,10 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.0) + isomorphic-ws: 5.0.0(ws@8.20.1) sync-fetch: 0.6.0-2 tslib: 2.8.1 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@fastify/websocket' - '@types/node' @@ -16730,7 +16829,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@js-joda/core@5.6.5': {} + '@js-joda/core@5.7.0': {} '@js-sdsl/ordered-map@4.4.2': {} @@ -16845,6 +16944,14 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.211.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.217.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -16855,10 +16962,20 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + yaml: 2.8.3 + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -16879,110 +16996,111 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/instrumentation-dataloader@0.17.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17028,22 +17146,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.211.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.7.4 - shimmer: 1.2.1 + '@opentelemetry/api-logs': 0.211.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/api-logs': 0.217.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 @@ -17052,29 +17176,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) protobufjs: 7.5.8 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': @@ -17082,21 +17206,39 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': @@ -17112,29 +17254,40 @@ snapshots: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color @@ -17145,11 +17298,11 @@ snapshots: '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': @@ -17162,11 +17315,18 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) semver: 7.7.4 - '@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.25.1': {} @@ -18589,7 +18749,7 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/readable-stream@4.0.22': + '@types/readable-stream@4.0.23': dependencies: '@types/node': 24.10.1 @@ -18745,7 +18905,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -18763,7 +18923,7 @@ snapshots: sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - msw @@ -19180,17 +19340,17 @@ snapshots: normalize-path: 3.0.0 picomatch: 4.0.4 - apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.0): + apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 - qs: 6.15.0 + qs: 6.15.2 applicationinsights@2.9.8: dependencies: '@azure/core-auth': 1.7.2 '@azure/core-rest-pipeline': 1.16.3 - '@azure/opentelemetry-instrumentation-azure-sdk': 1.0.0-beta.9 + '@azure/opentelemetry-instrumentation-azure-sdk': 1.0.0 '@microsoft/applicationinsights-web-snippet': 1.0.1 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) @@ -19245,7 +19405,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -19327,7 +19487,7 @@ snapshots: transitivePeerDependencies: - debug - azurite@3.35.0: + azurite@3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1): dependencies: '@azure/ms-rest-js': 1.11.2 applicationinsights: 2.9.8 @@ -19337,22 +19497,24 @@ snapshots: express: 4.22.1 fs-extra: 11.3.2 glob-to-regexp: 0.4.1 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 lokijs: 1.5.12 morgan: 1.10.1 multistream: 2.1.1 - mysql2: 3.15.3 + mysql2: 3.22.3(@types/node@24.10.1) rimraf: 3.0.2 - sequelize: 6.37.7(mysql2@3.15.3)(tedious@16.7.1) + sequelize: 6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)) stoppable: 1.1.0 - tedious: 16.7.1 + tedious: 16.7.1(@azure/core-client@1.10.1) to-readable-stream: 2.1.0 tslib: 2.8.1 uri-templates: 0.2.0 - uuid: 14.0.0 - winston: 3.18.3 + uuid: 3.4.0 + winston: 3.19.0 xml2js: 0.6.2 transitivePeerDependencies: + - '@azure/core-client' + - '@types/node' - applicationinsights-native-metrics - debug - ibm_db @@ -19433,9 +19595,9 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bl@6.1.5: + bl@6.1.6: dependencies: - '@types/readable-stream': 4.0.22 + '@types/readable-stream': 4.0.23 buffer: 6.0.3 inherits: 2.0.4 readable-stream: 4.7.0 @@ -19450,13 +19612,30 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.0 + qs: 6.15.2 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -19465,7 +19644,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.15.0 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -19511,7 +19690,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -19611,6 +19790,13 @@ snapshots: get-intrinsic: 1.3.0 set-function-length: 1.2.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -19756,6 +19942,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} + class-transformer@0.5.1: {} classnames@2.5.1: {} @@ -20363,7 +20551,7 @@ snapshots: dotenv@16.6.1: {} - dottie@2.0.6: {} + dottie@2.0.7: {} dset@3.1.4: {} @@ -20433,7 +20621,7 @@ snapshots: dependencies: stackframe: 1.3.4 - es-abstract@1.24.0: + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -20475,7 +20663,7 @@ snapshots: object.assign: 4.1.7 own-keys: 1.0.1 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 @@ -20494,7 +20682,7 @@ snapshots: dependencies: define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 function-bind: 1.1.2 globalthis: 1.0.4 @@ -20712,9 +20900,9 @@ snapshots: expect-type@1.3.0: {} - express-rate-limit@8.5.1(express@4.22.1): + express-rate-limit@8.5.1(express@4.22.2): dependencies: - express: 4.22.1 + express: 4.22.2 ip-address: 10.2.0 express@4.22.1: @@ -20740,7 +20928,43 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.15.0 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.2 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -21205,11 +21429,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.20.0 + ws: 8.20.1 graphql@14.7.0: dependencies: @@ -21527,6 +21751,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + icss-utils@5.1.0(postcss@8.5.10): dependencies: postcss: 8.5.10 @@ -21560,6 +21788,20 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + import-lazy@4.0.0: {} imurmurhash@0.1.4: {} @@ -21850,9 +22092,9 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.20.0): + isomorphic-ws@5.0.0(ws@8.20.1): dependencies: - ws: 8.20.0 + ws: 8.20.1 istanbul-lib-coverage@3.2.2: {} @@ -21948,7 +22190,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.0 + ws: 8.20.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -21991,9 +22233,9 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.2: + jsonwebtoken@9.0.3: dependencies: - jws: 3.2.2 + jws: 4.0.1 lodash.includes: 4.3.0 lodash.isboolean: 3.0.3 lodash.isinteger: 4.0.4 @@ -22004,24 +22246,13 @@ snapshots: ms: 2.1.3 semver: 7.7.4 - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - - jws@4.0.0: + jws@4.0.1: dependencies: jwa: 2.0.1 safe-buffer: 5.2.1 @@ -22266,17 +22497,13 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.3.3: {} - lru-cache@11.3.5: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@7.18.3: {} - - lru.min@1.1.3: {} + lru.min@1.1.4: {} luxon@3.7.2: {} @@ -22883,7 +23110,7 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -23023,17 +23250,17 @@ snapshots: mute-stream@0.0.8: {} - mysql2@3.15.3: + mysql2@3.22.3(@types/node@24.10.1): dependencies: + '@types/node': 24.10.1 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 long: 5.3.2 - lru.min: 1.1.3 - named-placeholders: 1.1.3 - seq-queue: 0.0.5 - sqlstring: 2.3.3 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 mz@2.7.0: dependencies: @@ -23041,9 +23268,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - named-placeholders@1.1.3: + named-placeholders@1.1.6: dependencies: - lru-cache: 7.18.3 + lru.min: 1.1.4 nanoid@3.3.11: {} @@ -23466,7 +23693,7 @@ snapshots: pend@1.2.0: {} - pg-connection-string@2.9.1: {} + pg-connection-string@2.13.0: {} picocolors@1.1.1: {} @@ -24080,7 +24307,7 @@ snapshots: pvutils@1.1.5: {} - qs@6.15.0: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -24099,6 +24326,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -24321,7 +24555,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -24498,6 +24732,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + require-like@0.1.2: {} requires-port@1.0.0: {} @@ -24636,9 +24877,9 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: - call-bind: 1.0.8 + call-bind: 1.0.9 call-bound: 1.0.4 get-intrinsic: 1.3.0 has-symbols: 1.1.0 @@ -24750,31 +24991,29 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 - seq-queue@0.0.5: {} - sequelize-pool@7.1.0: {} - sequelize@6.37.7(mysql2@3.15.3)(tedious@16.7.1): + sequelize@6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)): dependencies: '@types/debug': 4.1.12 '@types/validator': 13.15.10 debug: 4.4.3(supports-color@8.1.1) - dottie: 2.0.6 + dottie: 2.0.7 inflection: 1.13.4 lodash: 4.18.1 moment: 2.30.1 moment-timezone: 0.5.48 - pg-connection-string: 2.9.1 + pg-connection-string: 2.13.0 retry-as-promised: 7.1.1 semver: 7.7.4 sequelize-pool: 7.1.0 toposort-class: 1.0.1 - uuid: 14.0.0 - validator: 13.15.23 + uuid: 8.3.2 + validator: 13.15.35 wkx: 0.5.0 optionalDependencies: - mysql2: 3.15.3 - tedious: 16.7.1 + mysql2: 3.22.3(@types/node@24.10.1) + tedious: 16.7.1(@azure/core-client@1.10.1) transitivePeerDependencies: - supports-color @@ -24965,7 +25204,7 @@ snapshots: sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 - uuid: 14.0.0 + uuid: 8.3.2 websocket-driver: 0.7.4 sort-css-media-queries@2.2.0: {} @@ -25034,7 +25273,7 @@ snapshots: sprintf-js@1.1.3: {} - sqlstring@2.3.3: {} + sql-escaper@1.3.3: {} srcset@4.0.0: {} @@ -25067,9 +25306,9 @@ snapshots: stoppable@1.1.0: {} - storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): + storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 react: 19.2.0 @@ -25086,7 +25325,7 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.12) recast: 0.23.11 semver: 7.7.4 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - '@testing-library/dom' - bufferutil @@ -25133,7 +25372,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.0 + es-abstract: 1.24.2 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -25312,12 +25551,12 @@ snapshots: - bare-abort-controller - react-native-b4a - tedious@16.7.1: + tedious@16.7.1(@azure/core-client@1.10.1): dependencies: '@azure/identity': 3.4.2 - '@azure/keyvault-keys': 4.10.0 - '@js-joda/core': 5.6.5 - bl: 6.1.5 + '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) + '@js-joda/core': 5.7.0 + bl: 6.1.6 es-aggregate-error: 1.0.14 iconv-lite: 0.6.3 js-md4: 0.3.2 @@ -25326,6 +25565,7 @@ snapshots: node-abort-controller: 3.1.1 sprintf-js: 1.1.3 transitivePeerDependencies: + - '@azure/core-client' - supports-color terser-webpack-plugin@5.3.14(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)): @@ -25763,7 +26003,15 @@ snapshots: utils-merge@1.0.1: {} - uuid@14.0.0: {} + uuid@10.0.0: {} + + uuid@11.0.5: {} + + uuid@11.1.1: {} + + uuid@3.4.0: {} + + uuid@8.3.2: {} v8-to-istanbul@9.3.0: dependencies: @@ -25778,7 +26026,7 @@ snapshots: validate-npm-package-name@7.0.2: {} - validator@13.15.23: {} + validator@13.15.35: {} value-equal@1.0.1: {} @@ -25990,7 +26238,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.27.4)) - ws: 8.20.0 + ws: 8.20.1 optionalDependencies: webpack: 5.105.4(esbuild@0.27.4) transitivePeerDependencies: @@ -26153,7 +26401,7 @@ snapshots: readable-stream: 3.6.2 triple-beam: 1.4.1 - winston@3.18.3: + winston@3.19.0: dependencies: '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.8 @@ -26200,7 +26448,7 @@ snapshots: ws@7.5.10: {} - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.1.0: dependencies: @@ -26210,18 +26458,18 @@ snapshots: xml-js@1.6.11: dependencies: - sax: 1.4.3 + sax: 1.5.0 xml-name-validator@5.0.0: {} xml2js@0.4.23: dependencies: - sax: 1.4.3 + sax: 1.5.0 xmlbuilder: 11.0.1 xml2js@0.6.2: dependencies: - sax: 1.4.3 + sax: 1.5.0 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ac9ea4615..63e1e511e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -65,7 +65,8 @@ overrides: rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 'brace-expansion@1.1.12': 1.1.13 - 'brace-expansion@5.0.4': 5.0.5 + 'brace-expansion@5.0.4': 5.0.6 + 'brace-expansion@5.0.5': 5.0.6 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -76,7 +77,7 @@ overrides: svgo: ^3.3.3 'yaml@2.8.2': 2.8.3 'yauzl@3.2.0': 3.2.1 - qs: ^6.14.2 + qs: 6.15.2 'ajv@^6': 6.14.0 lodash: 4.18.1 lodash-es: 4.18.1 @@ -86,7 +87,9 @@ overrides: webpack: ^5.105.4 webpack-dev-server: ^5.2.4 express-rate-limit: 8.5.1 - uuid: 14.0.0 + '@azure/ms-rest-js>uuid': '^3.4.0' + 'azurite>uuid': '^3.4.0' + 'ws@8.20.0': 8.20.1 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 diff --git a/turbo.json b/turbo.json index 0ef199ae0..8bef61034 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", + "globalPassThroughEnv": ["WORKTREE_NAME"], "futureFlags": { "affectedUsingTaskInputs": true, "watchUsingTaskInputs": true @@ -101,6 +102,12 @@ "cache": false, "persistent": true }, + "dev:portless": { + "description": "Starts dev servers with worktree-scoped portless hostnames for git worktree isolation", + "dependsOn": ["^build"], + "cache": false, + "persistent": true + }, "azurite": { "description": "Starts the Azurite storage emulator", "cache": false, From b4db123ea16af23cb539ea8de5ce0db682489477 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 21 May 2026 10:27:20 -0400 Subject: [PATCH 03/40] fixes for e2e tests along with overall pattern cleanup --- apps/api/package.json | 4 +- apps/api/start-dev.mjs | 16 +- apps/api/turbo.json | 2 +- apps/docs/package.json | 2 +- apps/docs/turbo.json | 2 +- apps/server-mongodb-memory-mock/package.json | 2 +- apps/server-mongodb-memory-mock/turbo.json | 2 +- apps/server-oauth2-mock/package.json | 2 +- apps/server-oauth2-mock/start-dev.mjs | 18 +- apps/server-oauth2-mock/turbo.json | 2 +- apps/ui-community/package.json | 2 +- apps/ui-community/start-dev.mjs | 19 +- apps/ui-community/turbo.json | 2 +- apps/ui-staff/package.json | 2 +- apps/ui-staff/start-dev.mjs | 16 +- .../scripts/portless-hostnames.d.mts | 13 + build-pipeline/scripts/worktree-ports.d.mts | 5 + package.json | 4 +- .../ocom-verification/e2e-tests/cucumber.js | 2 + .../ocom-verification/e2e-tests/package.json | 4 +- .../src/shared/support/oauth2-login.ts | 6 +- .../shared/support/servers/e2e-defaults.ts | 65 +++++ .../src/shared/support/servers/index.ts | 2 +- .../shared/support/servers/portless-server.ts | 13 +- .../shared/support/servers/test-api-server.ts | 53 ++-- .../support/servers/test-azurite-server.ts | 105 +++++++ .../support/servers/test-environment.ts | 19 +- .../shared/support/shared-infrastructure.ts | 20 +- .../verification-shared/src/servers/index.ts | 2 +- .../src/servers/test-mongodb-server.ts | 3 +- .../src/servers/test-server.interface.ts | 26 +- .../src/settings/local-settings.ts | 31 +-- .../src/settings/portless-settings.ts | 68 +---- pnpm-lock.yaml | 258 ++++++++++++++++-- readme.md | 2 - turbo.json | 2 +- 36 files changed, 566 insertions(+), 230 deletions(-) create mode 100644 build-pipeline/scripts/portless-hostnames.d.mts create mode 100644 build-pipeline/scripts/worktree-ports.d.mts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts diff --git a/apps/api/package.json b/apps/api/package.json index 6041983b2..122cb1ca1 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,8 +10,8 @@ "build": "tsgo --build && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs", - "predev:portless": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev:portless": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings", + "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 978b8744b..9dc871786 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -22,15 +22,19 @@ const childEnv = { // Only inject worktree-scoped overrides when running in worktree mode. // When WORKTREE_NAME is absent, local.settings.json remains the source of truth. +// Use `??=` so callers (e.g. e2e harness with a MongoMemoryServer port) can override +// any individual value via process.env before invoking this script. if (process.env.WORKTREE_NAME) { const hostnames = getHostnames(); - childEnv.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community'); - childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); - childEnv.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); - childEnv.AZURE_STORAGE_CONNECTION_STRING = getAzuriteConnectionString(); - childEnv.AzureWebJobsStorage = getAzuriteConnectionString(); + childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community'); + childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); + childEnv.STAFF_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/staff'); + childEnv.STAFF_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); + childEnv.COSMOSDB_CONNECTION_STRING ??= getMongoConnectionString(); + childEnv.AZURE_STORAGE_CONNECTION_STRING ??= getAzuriteConnectionString(); + childEnv.AzureWebJobsStorage ??= getAzuriteConnectionString(); // Disable the Node.js inspector — port 5858 is already used by the primary worktree. - childEnv.languageWorkers__node__arguments = ''; + childEnv.languageWorkers__node__arguments ??= ''; } const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 0f908f3c8..6487a447c 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -12,7 +12,7 @@ "interruptible": true, "inputs": [] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "interruptible": true, "inputs": [] diff --git a/apps/docs/package.json b/apps/docs/package.json index 10766a652..9e8348ac1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs", - "dev:portless": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "docusaurus start --port 3001", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index d39e3fdc8..304f1bb57 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -7,7 +7,7 @@ "interruptible": false, "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": [], "persistent": true, "interruptible": false, diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index 2372d4635..ed4a774d3 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -12,7 +12,7 @@ "format:check": "biome format .", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "dev:portless": "node start-mongo.mjs" + "dev:worktree": "node start-mongo.mjs" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", diff --git a/apps/server-mongodb-memory-mock/turbo.json b/apps/server-mongodb-memory-mock/turbo.json index b0f2f18a9..5aea8cad2 100644 --- a/apps/server-mongodb-memory-mock/turbo.json +++ b/apps/server-mongodb-memory-mock/turbo.json @@ -8,7 +8,7 @@ "interruptible": true, "inputs": [] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "persistent": true, "interruptible": true, diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index bc92782ff..7201d84b9 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -12,7 +12,7 @@ "format:check": "biome format .", "start": "node dist/index.js", "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts", - "dev:portless": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest" diff --git a/apps/server-oauth2-mock/start-dev.mjs b/apps/server-oauth2-mock/start-dev.mjs index b6434763a..2bd4f4b1e 100644 --- a/apps/server-oauth2-mock/start-dev.mjs +++ b/apps/server-oauth2-mock/start-dev.mjs @@ -2,17 +2,19 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +const childEnv = { ...process.env }; + +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.BASE_URL = buildPortlessUrl(hostnames.mockAuth); + // Override redirect URIs so portal-discovery picks up worktree-scoped URLs. + childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'); + childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'); +} const child = spawn('tsx', ['src/index.ts'], { stdio: 'inherit', - env: { - ...process.env, - BASE_URL: buildPortlessUrl(hostnames.mockAuth), - // Override redirect URIs so portal-discovery picks up worktree-scoped URLs - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/apps/server-oauth2-mock/turbo.json b/apps/server-oauth2-mock/turbo.json index 325758390..2596ed01a 100644 --- a/apps/server-oauth2-mock/turbo.json +++ b/apps/server-oauth2-mock/turbo.json @@ -10,7 +10,7 @@ "persistent": true, "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["build"], "interruptible": true, "persistent": true, diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 86bc14f1d..ac0f7010d 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -10,7 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless ownercommunity.localhost --force vite", - "dev:portless": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index a13661535..5226fb736 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -2,17 +2,20 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +const childEnv = { ...process.env }; + +// Worktree-scoped overrides; plain `dev` leaves .env as the source of truth. +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/community'); + childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'); + childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); + childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); +} const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { stdio: 'inherit', - env: { - ...process.env, - VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/community'), - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), - VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), - VITE_APP_UI_COMMUNITY_BASE_URL: buildPortlessUrl(hostnames.uiCommunity), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index e253b58b7..b7c32402e 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -7,7 +7,7 @@ "interruptible": false, "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] }, - "dev:portless": { + "dev:worktree": { "dependsOn": ["^build"], "persistent": true, "interruptible": false, diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index bd35eaf13..9b8757b90 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -10,7 +10,7 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", - "dev:portless": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 4b656ce70..eaa17cdb8 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -2,16 +2,18 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -const hostnames = getHostnames(); +const childEnv = { ...process.env }; + +if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + childEnv.VITE_APP_UI_STAFF_AAD_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/staff'); + childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'); + childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); +} const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { stdio: 'inherit', - env: { - ...process.env, - VITE_APP_UI_STAFF_AAD_AUTHORITY: buildPortlessUrl(hostnames.mockAuth, '/staff'), - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), - VITE_COMMON_API_ENDPOINT: buildPortlessUrl(hostnames.api, '/api/graphql'), - }, + env: childEnv, }); child.on('exit', (code, signal) => { diff --git a/build-pipeline/scripts/portless-hostnames.d.mts b/build-pipeline/scripts/portless-hostnames.d.mts new file mode 100644 index 000000000..139d75e94 --- /dev/null +++ b/build-pipeline/scripts/portless-hostnames.d.mts @@ -0,0 +1,13 @@ +export const PORTLESS_PORT: number; + +export interface PortlessHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +export function getHostnames(): PortlessHostnames; + +export function buildPortlessUrl(hostname: string, path?: string): string; diff --git a/build-pipeline/scripts/worktree-ports.d.mts b/build-pipeline/scripts/worktree-ports.d.mts new file mode 100644 index 000000000..c2cbdf86e --- /dev/null +++ b/build-pipeline/scripts/worktree-ports.d.mts @@ -0,0 +1,5 @@ +export function getWorktreePortOffset(): number; +export function getMongoPort(): number; +export function getAzuritePorts(): { blob: number; queue: number; table: number }; +export function getAzuriteConnectionString(): string; +export function getMongoConnectionString(): string; diff --git a/package.json b/package.json index c1757bc2c..3dc82ec64 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "test": "turbo run test", "lint": "turbo run lint", "dev": "pnpm proxy:stop && pnpm proxy:start && turbo watch azurite dev --filter='./apps/*' --filter='./packages/*'", - "dev:portless": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:portless --filter='./apps/*' --filter='./packages/*'", + "dev:worktree": "WORKTREE_NAME=$(basename $PWD) pnpm proxy:ensure && WORKTREE_NAME=$(basename $PWD) turbo watch azurite dev:worktree --filter='./apps/*' --filter='./packages/*'", "start": "turbo run build && concurrently --kill-others-on-fail \"pnpm run start:api\" \"pnpm run start:ui-community\"", "proxy:stop": "pnpm exec portless proxy stop || true", "proxy:start": "pnpm exec portless proxy start --https -p 1355", @@ -33,7 +33,7 @@ "test:coverage": "turbo run test:coverage", "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", - "test:e2e:portless": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:worktree": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index 51912f713..e548d91e5 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -7,5 +7,7 @@ export default { formatOptions: { snippetInterface: 'async-await', }, + // Disable parallel workers — the shared portless proxy and per-worktree port + // scheme make parallel browsers contend for the same hostnames. parallel: 0, }; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index 6d3508bc6..cbe99de57 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,9 +5,7 @@ "private": true, "type": "module", "scripts": { - "test:e2e": "pnpm run proxy:start && pnpm run test:e2e:run", - "test:e2e:run": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", - "proxy:start": "pnpm exec portless proxy start -p 1355", + "test:e2e": "NODE_EXTRA_CA_CERTS=${HOME}/.portless/ca.pem LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js", "playwright:install": "playwright install chromium", "clean": "rimraf dist reports target" }, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index abc2b4638..3bc2fccf8 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -1,3 +1,4 @@ +import { actors } from '@ocom-verification/verification-shared/test-data'; import { type Actor, Interaction, the } from '@serenity-js/core'; import type { Page } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; @@ -32,9 +33,10 @@ export async function performOAuth2Login(page: Page): Promise { // Wait for redirects to settle on either the login page or the app await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); - // If the mock OAuth2 login form is shown, fill credentials and submit + // If the mock OAuth2 login form is shown, fill credentials and submit. + // CommunityOwner is defined in mock-oidc.users.json with password "password". if (page.url().includes('/login')) { - await page.fill('input[name="username"]', 'test@example.com'); + await page.fill('input[name="username"]', actors.CommunityOwner.email); await page.fill('input[name="password"]', 'password'); await page.click('button[type="submit"]'); } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts new file mode 100644 index 000000000..6680d32a8 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts @@ -0,0 +1,65 @@ +// biome-ignore format: keep the public Azurite dev key split across chunks for static analysis. + +/** + * Non-secret environment defaults for the e2e harness. + * + * Used when `apps/api/local.settings.json` is absent (typical in CI). Every + * value here is either a public Azurite development credential or a mock-only + * constant — none of them are valid in any real environment, and they are + * never applied outside the e2e harness. + * + * Worktree-scoped values (OIDC URLs, mongo/azurite ports, connection strings) + * are computed at runtime by `build-pipeline/scripts/portless-hostnames.mjs` + * and `worktree-ports.mjs`; they intentionally do NOT live here. + */ + +const AZURITE_ACCOUNT_KEY = [ + 'Eby8vdM02xNOcqFlqUwJPLlm', + 'EtlCDXJ1OUzFT50uSRZ6IFs', + 'uFq2UVErCz4I6tq/K1SZFP', + 'TOtr/KBHBeksoGMGw==', +].join(''); + +const E2E_API_DEFAULTS = { + FUNCTIONS_WORKER_RUNTIME: 'node', + NODE_ENV: 'development', + CONFIG_VERSION: '3.0', + + ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', + ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', + STAFF_PORTAL_OIDC_AUDIENCE: 'mock-client', + STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', + + COSMOSDB_DBNAME: 'owner-community', + + // Well-known Azurite dev account — documented in Azurite's README, not a secret. + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: AZURITE_ACCOUNT_KEY, +} as const; + +/** + * Apply e2e defaults to `process.env` for any key not already set. A pre-existing + * env var (from the developer's shell or a copied local.settings.json) always wins. + * Spawned child processes inherit `process.env`, so this propagates to azurite, + * the api function host, and the oauth2 mock without any per-server plumbing. + */ +export function applyE2EDefaultsToEnv(): void { + for (const [key, value] of Object.entries(E2E_API_DEFAULTS)) { + process.env[key] ??= value; + } +} + +/** + * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. + * + * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript + * source is executed directly. Child processes spawned by the test harness + * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on + * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate + * crash. Callers can spread additional overrides on top of the result. + */ +export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { + const env = { ...process.env, ...overrides }; + delete env['NODE_OPTIONS']; + return env; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts index c096f6636..25d4dffc6 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts @@ -1,12 +1,12 @@ export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; export { PortlessServer } from './portless-server.ts'; export { TestApiServer } from './test-api-server.ts'; +export { TestAzuriteServer } from './test-azurite-server.ts'; export { TestCommunityViteServer } from './test-community-vite-server.ts'; export { buildUrl, cleanupTestEnvironment, initTestEnvironment, - mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, setMongoConnectionString, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index aea6c3ab1..6ff6ace04 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,6 +1,7 @@ import { type ChildProcess, spawn } from 'node:child_process'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; import { getTimeout } from '@ocom-verification/verification-shared/settings'; +import { spawnEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; /** @@ -68,16 +69,9 @@ export abstract class PortlessServer implements TestServer { if (this.process || this.startedByUs) return; if (await this.isAlreadyRunning()) return; - const env = { - ...process.env, - ...this.extraEnv, - }; - // Remove NODE_OPTIONS from child process to avoid tsx import issues - delete env['NODE_OPTIONS']; - this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, - env, + env: spawnEnv(this.extraEnv), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -159,7 +153,8 @@ export abstract class PortlessServer implements TestServer { }); proc.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); + const text = data.toString(); + stderrOutput += text; }); proc.on('error', (err: Error) => { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 380249d50..a92d3f893 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,25 +1,44 @@ import { execFileSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { spawnEnv } from './e2e-defaults.ts'; import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getHostnames, getMongoConnectionString, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +import { buildUrl, getHostnames, getMongoConnectionString } from './test-environment.ts'; const hostnames = getHostnames(); +/** + * Spawns the api dev server the same way `pnpm dev:worktree` does. The + * worktree-aware overrides (OIDC URLs, Azurite connection, etc.) all come + * from apps/api/start-dev.mjs — we only inject the dynamic MongoDB + * connection string from MongoMemoryServer here. + */ export class TestApiServer extends PortlessServer { override async start(): Promise { - // Mirror the app's real dev bootstrap so deploy assets and local settings - // stay in sync with recent package-script changes. - const env = { - ...process.env, - }; - delete env['NODE_OPTIONS']; - - execFileSync('pnpm', ['run', 'predev'], { + // Mirror the `predev:worktree` lifecycle hook so deploy/ and local.settings.json + // stay in sync — start-dev.mjs is invoked directly here, bypassing pnpm's pre-hook. + execFileSync('pnpm', ['run', 'predev:worktree'], { cwd: this.cwd, - env, + env: spawnEnv(), stdio: 'pipe', }); + // Patch deploy/local.settings.json with e2e-specific values. + // Azure Functions loads local.settings.json values into the worker env, + // overriding any parent-process env vars. We must patch the file so the + // worker gets the MongoMemoryServer connection string (random port) and + // the inspector is disabled (port 5858 may already be in use). + const settingsPath = join(this.cwd, 'deploy', 'local.settings.json'); + try { + const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); + settings.Values.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); + settings.Values['languageWorkers__node__arguments'] = ''; + writeFileSync(settingsPath, JSON.stringify(settings, null, '\t')); + } catch { + /* best-effort — file may not exist in CI */ + } + await super.start(); } @@ -59,21 +78,9 @@ export class TestApiServer extends PortlessServer { } protected override get extraEnv() { + // start-dev.mjs handles the rest via WORKTREE_NAME + `??=` fallbacks. return { - NODE_ENV: 'development', - languageWorkers__node__arguments: '', COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - COSMOSDB_DBNAME: apiSettings.cosmosDbName, - AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', - ACCOUNT_PORTAL_OIDC_ISSUER: mockOidcIssuer, - ACCOUNT_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, - ACCOUNT_PORTAL_OIDC_AUDIENCE: mockOidcAudience, - ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', - STAFF_PORTAL_OIDC_ISSUER: mockOidcIssuer, - STAFF_PORTAL_OIDC_ENDPOINT: mockOidcEndpoint, - STAFF_PORTAL_OIDC_AUDIENCE: mockOidcAudience, - STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', - VITE_COMMON_API_ENDPOINT: buildUrl(hostnames.api, '/api/graphql'), }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts new file mode 100644 index 000000000..d43d9efd3 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -0,0 +1,105 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import net from 'node:net'; +import { join } from 'node:path'; +import type { TestServer } from '@ocom-verification/verification-shared/servers'; +import { apiSettings, getTimeout } from '@ocom-verification/verification-shared/settings'; +import { getAzuritePorts } from '../../../../../../../build-pipeline/scripts/worktree-ports.mjs'; +import { spawnEnv } from './e2e-defaults.ts'; + +/** + * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits + * if the blob port is already listening, so concurrent worktrees and re-runs are + * safe. We track the spawned process only when we started it ourselves. + */ +export class TestAzuriteServer implements TestServer { + private process: ChildProcess | null = null; + private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; + + private get blobPort(): number { + return getAzuritePorts().blob; + } + + async start(): Promise { + if (this.process || this.startedByUs) return; + if (await isPortListening(this.blobPort)) return; + + const binDir = join(apiSettings.apiDir, 'node_modules', '.bin'); + + this.process = spawn('node', ['start-azurite.mjs'], { + cwd: apiSettings.apiDir, + env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), + detached: this.useDetachedProcessGroup, + stdio: ['ignore', 'pipe', 'pipe'], + }); + this.startedByUs = true; + + await this.waitForReady(); + } + + async stop(): Promise { + if (!this.process || !this.startedByUs) return; + + const proc = this.process; + this.process = null; + this.startedByUs = false; + + killProcess(proc, 'SIGTERM', this.useDetachedProcessGroup); + + await new Promise((resolve) => { + const timeout = setTimeout(() => { + killProcess(proc, 'SIGKILL', this.useDetachedProcessGroup); + resolve(); + }, getTimeout('serverShutdown')); + + proc.on('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } + + isRunning(): boolean { + return this.process !== null; + } + + getUrl(): string { + return `http://127.0.0.1:${this.blobPort}`; + } + + private async waitForReady(): Promise { + const deadline = Date.now() + getTimeout('serverStartup'); + const interval = getTimeout('healthProbeInterval'); + while (Date.now() < deadline) { + if (await isPortListening(this.blobPort)) return; + await new Promise((resolve) => setTimeout(resolve, interval)); + } + throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); + } +} + +function isPortListening(port: number): Promise { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +function killProcess(proc: ChildProcess, signal: NodeJS.Signals, useGroup: boolean): void { + if (useGroup && proc.pid) { + try { + process.kill(-proc.pid, signal); + return; + } catch { + /* Fall back to killing the direct child. */ + } + } + proc.kill(signal); +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 4a8fbb54f..6550beb7f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,11 +1,12 @@ import { execFileSync } from 'node:child_process'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; +import { applyE2EDefaultsToEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; -/** Module-level hostnames derived from .env files (matches dev:portless pattern). */ +/** Module-level hostnames derived from .env files (matches dev:worktree pattern). */ const hostnames = getHostnames(); /** OIDC issuer URL for the community portal on the mock auth server. */ @@ -14,19 +15,19 @@ export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community') /** JWKS endpoint used as the OIDC discovery / probe URL. */ export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; -/** Audience claim expected in JWTs issued by the mock OIDC server. */ -export const mockOidcAudience = 'mock-client'; - /** - * Prune orphaned portless route locks from previous test runs. - * The proxy itself is started by the `test:e2e` npm script before the - * Cucumber process spawns, so we only need to clean stale locks here. + * Apply e2e env defaults (so CI runs without local.settings.json) and ensure + * the portless proxy is running. The `proxy start` invocation is idempotent — + * if another worktree or dev session already started it on the shared port, + * this returns immediately. */ export function initTestEnvironment() { if (proxyInitialized) return; - execFileSync(getPortlessPath(), ['prune'], { - timeout: 10_000, + applyE2EDefaultsToEnv(); + + execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { + timeout: 15_000, stdio: 'pipe', }); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index e91cca9b9..7a793aa96 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -1,9 +1,10 @@ import playwright, { type Browser, type BrowserContext } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestAzuriteServer, TestCommunityViteServer, TestOAuth2Server } from './servers/index.ts'; let mongoDBServer: MongoDBTestServer | undefined; +let azuriteServer: TestAzuriteServer | undefined; let oauth2Server: TestOAuth2Server | undefined; let apiServer: TestApiServer | undefined; let viteServer: TestCommunityViteServer | undefined; @@ -66,6 +67,10 @@ export async function stopAll(): Promise { await mongoDBServer.stop().catch(() => undefined); mongoDBServer = undefined; } + if (azuriteServer) { + await azuriteServer.stop().catch(() => undefined); + azuriteServer = undefined; + } apiUrl = undefined; browserBaseUrl = undefined; cleanupTestEnvironment(); @@ -75,14 +80,23 @@ export async function ensureE2EServers(): Promise { registerShutdownHandlers(); initTestEnvironment(); - // Phase 1: Start MongoDB and OAuth2 in parallel (no interdependency) + // Phase 1: Start MongoDB, Azurite, and OAuth2 in parallel (no interdependency) mongoDBServer ??= new MongoDBTestServer(); + azuriteServer ??= new TestAzuriteServer(); oauth2Server ??= new TestOAuth2Server(); const mongo = mongoDBServer; + const azurite = azuriteServer; const oauth2 = oauth2Server; const phase1: Promise[] = []; if (!mongo.isRunning()) { - phase1.push(mongo.start().then(() => setMongoConnectionString(mongo.getConnectionString()))); + phase1.push( + mongo.start().then(() => { + setMongoConnectionString(mongo.getConnectionString()); + }), + ); + } + if (!azurite.isRunning()) { + phase1.push(azurite.start()); } if (!oauth2.isRunning()) { phase1.push(oauth2.start()); diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts index 32810914a..4313727fb 100644 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ b/packages/ocom-verification/verification-shared/src/servers/index.ts @@ -7,4 +7,4 @@ export { MongoDBTestServer, seedOwnerCommunityReferenceData, } from './test-mongodb-server.ts'; -export type { TestServer, TestServerOptions } from './test-server.interface.ts'; +export type { TestServer } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts index 37937e616..ca57fa2b9 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts @@ -6,6 +6,7 @@ import { getAllMockUsers } from '../test-data/index.ts'; const MONGO_BINARY_VERSION = '7.0.14'; const DEFAULT_DB_NAME = 'owner-community-test'; const MAX_REPLSET_START_ATTEMPTS = 5; +type MongoMemoryReplSetConfig = NonNullable[0]>; export type MongoDBSeedDataFunction = (connectionString: string, dbName: string) => Promise; @@ -122,7 +123,7 @@ export class MongoDBTestServer { return this.serviceMongoose !== null; } - private async createReplicaSetWithRetry(config: Parameters[0]): Promise { + private async createReplicaSetWithRetry(config: MongoMemoryReplSetConfig): Promise { let lastError: unknown; for (let attempt = 1; attempt <= MAX_REPLSET_START_ATTEMPTS; attempt += 1) { diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts index 8b08f6b92..16e024f23 100644 --- a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts +++ b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts @@ -1,15 +1,7 @@ /** * Common interface for all test servers (in-process and subprocess). - * - * This abstraction allows acceptance-api and e2e tests to use - * consistent server lifecycle management patterns while choosing - * the appropriate implementation: - * - * - **In-process** (GraphQLTestServer): Fast, isolated, mocked services - * Best for: API acceptance tests, unit-like integration tests - * - * - **Subprocess** (PortlessServer): Full stack, realistic, real services - * Best for: E2E tests, full system integration tests + * Implemented by GraphQLTestServer (in-process), PortlessServer (subprocess + * via the portless proxy), and TestAzuriteServer. */ export interface TestServer { /** Start the server and return when ready */ @@ -24,17 +16,3 @@ export interface TestServer { /** Get the server URL (throws if not running) */ getUrl(): string; } - -/** - * Configuration options for test server startup. - */ -export interface TestServerOptions { - /** Port to listen on (0 for random available port) */ - port?: number; - - /** Additional environment variables for subprocess servers */ - env?: Record; - - /** Timeout for server startup (defaults to centralized config) */ - startupTimeoutMs?: number; -} diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts index c28d7a6f5..8e9929f78 100644 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts @@ -8,36 +8,9 @@ const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); const apiValues = readJsonSettings(apiSettingsPath); const uiValues = readDotEnv(uiEnvPath); -/** - * Defaults for E2E/acceptance test settings when local.settings.json is absent - * (e.g. CI pipelines). All values are non-secret mock/localhost references used - * exclusively by the test harness — no real credentials are involved. - */ -const ciDefaults = { - COSMOSDB_CONNECTION_STRING: '', - COSMOSDB_DBNAME: 'owner-community', - COSMOSDB_PORT: '50000', - NODE_ENV: 'development', - ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', - ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', - ACCOUNT_PORTAL_OIDC_ENDPOINT: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', -} as const; - -function setting(key: keyof typeof ciDefaults): string { - return readSetting(apiValues, key, ciDefaults[key]) ?? ciDefaults[key]; -} - export const apiSettings = { - nodeEnv: setting('NODE_ENV'), - isDevelopment: setting('NODE_ENV') === 'development', - - cosmosDbConnectionString: setting('COSMOSDB_CONNECTION_STRING'), - cosmosDbName: setting('COSMOSDB_DBNAME'), - cosmosDbPort: Number(setting('COSMOSDB_PORT')), - - accountPortalOidcIssuer: setting('ACCOUNT_PORTAL_OIDC_ISSUER'), - accountPortalOidcEndpoint: setting('ACCOUNT_PORTAL_OIDC_ENDPOINT'), - accountPortalOidcAudience: setting('ACCOUNT_PORTAL_OIDC_AUDIENCE'), + cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING', '') ?? '', + cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', apiDir: path.dirname(apiSettingsPath), oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index e084702e0..405f1802e 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,65 +1,7 @@ /** - * Portless hostname derivation for test environments. - * - * Mirrors the logic of `build-pipeline/scripts/portless-hostnames.mjs` — - * hostnames are derived from the tracked .env files rather than hardcoded, - * and the `WORKTREE_NAME` suffix is applied automatically. - * - * This keeps test-environment code in sync with the dev:portless startup - * scripts without adding a cross-layer import to build-pipeline/. + * Re-export of the canonical portless hostname helpers used by both the + * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as + * the single source of truth means there is exactly one place that derives + * hostnames from .env and applies the WORKTREE_NAME suffix. */ - -import { findWorkspaceRoot, readDotEnv, resolveWorkspacePath } from './settings-utils.ts'; - -const PORTLESS_PORT = 1355; -const workspaceRoot = findWorkspaceRoot(); - -const uiCommunityEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env')); -const uiStaffEnv = readDotEnv(resolveWorkspacePath(workspaceRoot, 'apps/ui-staff/.env')); - -function hostnameFromUrl(url: string): string { - try { - return new URL(url).hostname; - } catch { - return ''; - } -} - -/** Splice `.` in before `.localhost` for worktree isolation. */ -function applyWorktreeSuffix(hostname: string): string { - const wt = process.env['WORKTREE_NAME']; - if (!wt) return hostname; - return hostname.replace('.localhost', `.${wt}.localhost`); -} - -/** - * Returns all portless service hostnames scoped to the current worktree. - * Hostnames are derived from the tracked .env files — no names are hardcoded. - */ -export function getHostnames() { - const uiCommunity = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); - const api = hostnameFromUrl(uiCommunityEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); - const mockAuth = hostnameFromUrl(uiCommunityEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); - const uiStaff = hostnameFromUrl(uiStaffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); - - if (!uiCommunity || !api || !mockAuth) { - throw new Error( - 'portless-settings: could not derive hostnames from .env files. ' + 'Ensure apps/ui-community/.env is present with VITE_APP_UI_COMMUNITY_BASE_URL, ' + 'VITE_COMMON_API_ENDPOINT, and VITE_APP_UI_COMMUNITY_B2C_AUTHORITY.', - ); - } - - return { - uiCommunity: applyWorktreeSuffix(uiCommunity), - uiStaff: uiStaff ? applyWorktreeSuffix(uiStaff) : '', - api: applyWorktreeSuffix(api), - mockAuth: applyWorktreeSuffix(mockAuth), - docs: applyWorktreeSuffix(`docs.${uiCommunity}`), - }; -} - -/** Build a full portless HTTPS URL from a hostname and optional path. */ -export function buildPortlessUrl(hostname: string, path = ''): string { - return `https://${hostname}:${PORTLESS_PORT}${path}`; -} - -export { PORTLESS_PORT }; +export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from '../../../../../build-pipeline/scripts/portless-hostnames.mjs'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a16e5e4be..8b33c6e41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -948,25 +948,25 @@ importers: version: link:../config-vitest '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-a11y': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-docs': specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) '@storybook/react': specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) '@storybook/react-vite': specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -978,10 +978,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitest/browser': specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) @@ -999,13 +999,13 @@ importers: version: 6.0.1 storybook: specifier: 'catalog:' - version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -14761,6 +14761,18 @@ snapshots: '@blazediff/core@1.9.1': {} + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@neoconfetti/react': 1.0.0 + chromatic: 13.3.4 + filesize: 10.1.6 + jsonfile: 6.2.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + strip-ansi: 7.1.2 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@neoconfetti/react': 1.0.0 @@ -15089,7 +15101,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -15125,7 +15137,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -15174,9 +15186,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -16796,6 +16808,15 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + glob: 10.5.0 + magic-string: 0.30.21 + react-docgen-typescript: 2.4.0(typescript@6.0.3) + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + optionalDependencies: + typescript: 6.0.3 + '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 10.5.0 @@ -18279,12 +18300,31 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@storybook/global': 5.0.0 + axe-core: 4.11.0 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -18298,6 +18338,10 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18318,6 +18362,22 @@ snapshots: - react - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + prompts: 2.4.2 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + optionalDependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/runner': 4.1.2 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - react + - react-dom + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': dependencies: '@storybook/global': 5.0.0 @@ -18334,6 +18394,13 @@ snapshots: - react - react-dom + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ts-dedent: 2.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -18341,6 +18408,11 @@ snapshots: ts-dedent: 2.2.0 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + unplugin: 1.16.1 + '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18353,12 +18425,38 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@rollup/pluginutils': 5.3.0 + '@storybook/builder-vite': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + find-up: 7.0.0 + magic-string: 0.30.21 + react: 19.2.0 + react-docgen: 8.0.2 + react-dom: 19.2.0(react@19.2.0) + resolve: 1.22.11 + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + tsconfig-paths: 4.2.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18379,6 +18477,16 @@ snapshots: - supports-color - typescript + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + optionalDependencies: + typescript: 6.0.3 + '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 @@ -18882,6 +18990,19 @@ snapshots: - vite optional: true + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + playwright: 1.59.0 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) @@ -18913,6 +19034,23 @@ snapshots: - vite optional: true + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@blazediff/core': 1.9.1 @@ -18942,7 +19080,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -18963,6 +19101,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 @@ -18979,6 +19125,14 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -25312,6 +25466,28 @@ snapshots: graphql: 16.12.0 react: 19.2.0 + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@storybook/global': 5.0.0 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/spy': 3.2.4 + better-opn: 3.0.2 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + recast: 0.23.11 + semver: 7.7.4 + ws: 8.20.1 + transitivePeerDependencies: + - '@testing-library/dom' + - bufferutil + - msw + - supports-color + - utf-8-validate + - vite + storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@storybook/global': 5.0.0 @@ -26069,6 +26245,26 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + esbuild: 0.25.12 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.3 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -26119,6 +26315,36 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.10.1 + '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 diff --git a/readme.md b/readme.md index c64caa156..c05851acc 100644 --- a/readme.md +++ b/readme.md @@ -26,8 +26,6 @@ Our Docusaurus website will help you get started in running and contributing to ## Developer usage - - - Full local dev (builds, starts the portless HTTPS proxy, starts Azurite, and runs the app-level dev servers): ```bash diff --git a/turbo.json b/turbo.json index 8bef61034..c1988720e 100644 --- a/turbo.json +++ b/turbo.json @@ -102,7 +102,7 @@ "cache": false, "persistent": true }, - "dev:portless": { + "dev:worktree": { "description": "Starts dev servers with worktree-scoped portless hostnames for git worktree isolation", "dependsOn": ["^build"], "cache": false, From 2d54c579b05fb58beca45b5f5d5d3519e1e037cc Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 26 May 2026 15:15:56 -0400 Subject: [PATCH 04/40] overall improvements to workspace files, along with implementation of settings modes, for e2e, so we have alternate between settings files --- .gitignore | 1 - .snyk | 5 + apps/api/local-settings.e2e.json | 29 + apps/api/package.json | 4 +- apps/api/scripts/sync-local-settings.mjs | 59 ++ apps/api/start-azurite.mjs | 30 +- apps/api/start-dev.mjs | 18 +- apps/api/turbo.json | 2 +- apps/docs/start-dev.mjs | 11 +- .../start-mongo.mjs | 18 +- apps/server-oauth2-mock/start-dev.mjs | 12 +- apps/ui-community/.env.e2e | 9 + apps/ui-community/start-dev.mjs | 20 +- apps/ui-community/turbo.json | 4 +- apps/ui-staff/.env.e2e | 8 + apps/ui-staff/start-dev.mjs | 20 +- biome.json | 2 +- build-pipeline/scripts/dev-process-exit.mjs | 8 - .../scripts/portless-hostnames.d.mts | 13 - build-pipeline/scripts/worktree-ports.d.mts | 5 - knip.json | 16 +- package.json | 2 +- .../mock-application-services.ts | 2 +- .../shared/support/shared-infrastructure.ts | 20 +- .../src/shared/support/ui/jsdom-setup.ts | 2 +- .../archunit-tests/src/validate-env-names.cjs | 11 + .../src/shared/support/servers/app-paths.ts | 9 + .../support/servers/child-process-env.ts | 14 + .../shared/support/servers/e2e-defaults.ts | 65 -- .../shared/support/servers/portless-server.ts | 2 +- .../shared/support/servers/test-api-server.ts | 46 +- .../support/servers/test-azurite-server.ts | 52 +- .../servers/test-community-vite-server.ts | 5 +- .../support/servers/test-environment.ts | 6 +- .../support/servers/test-oauth2-server.ts | 4 +- .../ocom-verification/e2e-tests/turbo.json | 28 +- .../verification-shared/src/settings/index.ts | 9 - .../src/settings/local-settings.ts | 24 - .../src/settings/portless-settings.ts | 22 +- .../src/settings/settings-utils.ts | 93 --- packages/ocom/service-otel/package.json | 2 +- pnpm-lock.yaml | 678 +++++------------- pnpm-workspace.yaml | 2 + scripts/local-dev/dev-process-exit.mjs | 40 ++ scripts/local-dev/port-ready.mjs | 45 ++ .../local-dev}/portless-hostnames.mjs | 30 +- .../local-dev}/worktree-ports.mjs | 42 +- sonar-project.properties | 2 +- 48 files changed, 636 insertions(+), 915 deletions(-) create mode 100644 apps/api/local-settings.e2e.json create mode 100644 apps/api/scripts/sync-local-settings.mjs create mode 100644 apps/ui-community/.env.e2e create mode 100644 apps/ui-staff/.env.e2e delete mode 100644 build-pipeline/scripts/dev-process-exit.mjs delete mode 100644 build-pipeline/scripts/portless-hostnames.d.mts delete mode 100644 build-pipeline/scripts/worktree-ports.d.mts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/local-settings.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/settings-utils.ts create mode 100644 scripts/local-dev/dev-process-exit.mjs create mode 100644 scripts/local-dev/port-ready.mjs rename {build-pipeline/scripts => scripts/local-dev}/portless-hostnames.mjs (81%) rename {build-pipeline/scripts => scripts/local-dev}/worktree-ports.mjs (77%) diff --git a/.gitignore b/.gitignore index 9bce6ceaf..33786a672 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,3 @@ apps/api/deploy/ # Build-time evidence artifacts (generated) build-artifacts/ - diff --git a/.snyk b/.snyk index 02a4e4b14..b473a7757 100644 --- a/.snyk +++ b/.snyk @@ -91,3 +91,8 @@ ignore: reason: 'Transitive dependency in @docusaurus/preset-classic; not exploitable in current usage.' expires: '2026-06-18T00:00:00.000Z' created: '2026-05-18T11:04:00.000Z' + 'SNYK-JS-POSTCSSSELECTORPARSER-16873882': + - '* > postcss-selector-parser': + reason: 'Transitive dependency in @docusaurus/core build pipeline; no upgrade or patch available from upstream. Not exploitable in current usage (build-time CSS processing only).' + expires: '2026-08-26T00:00:00.000Z' + created: '2026-05-26T00:00:00.000Z' diff --git a/apps/api/local-settings.e2e.json b/apps/api/local-settings.e2e.json new file mode 100644 index 000000000..174c23534 --- /dev/null +++ b/apps/api/local-settings.e2e.json @@ -0,0 +1,29 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "NODE_ENV": "development", + "languageWorkers__node__arguments": "", + "AZURE_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "ACCOUNT_PORTAL_OIDC_AUDIENCE": "mock-client", + "ACCOUNT_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json", + "ACCOUNT_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/community", + "ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER": true, + "APPLICATIONINSIGHTS_CONNECTION_STRING": "", + "CONFIG_VERSION": "3.0", + "COSMOSDB_CONNECTION_STRING": "mongodb://127.0.0.1:50000/owner-community?replicaSet=globaldb", + "COSMOSDB_DBNAME": "owner-community", + "STAFF_PORTAL_OIDC_AUDIENCE": "mock-client", + "STAFF_PORTAL_OIDC_ENDPOINT": "https://mock-auth.ownercommunity.localhost:1355/staff/.well-known/jwks.json", + "STAFF_PORTAL_OIDC_ISSUER": "https://mock-auth.ownercommunity.localhost:1355/staff", + "STAFF_PORTAL_OIDC_IGNORE_ISSUER": true, + "STORAGE_ACCOUNT_NAME": "devstoreaccount1", + "STORAGE_ACCOUNT_KEY": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" + }, + "ConnectionStrings": {}, + "Host": { + "LocalHttpPort": 7071, + "CORS": "*" + } +} diff --git a/apps/api/package.json b/apps/api/package.json index 122cb1ca1..3e940dba6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,6 +10,8 @@ "build": "tsgo --build && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs", + "predev:e2e": "pnpm run prepare:deploy && node scripts/sync-local-settings.mjs e2e", + "dev:e2e": "pnpm exec portless data-access.ownercommunity${WORKTREE_NAME:+.${WORKTREE_NAME}}.localhost --force node start-dev.mjs", "predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings", "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", "prepare:deploy": "cellix-prepare-func-deploy", @@ -23,7 +25,7 @@ "clean": "rimraf dist deploy", "prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings", "start": "func start --typescript --script-root deploy/", - "sync-local-settings": "node -e \"const fs=require('node:fs'); fs.mkdirSync('deploy',{recursive:true}); if (fs.existsSync('local.settings.json')) fs.copyFileSync('local.settings.json','deploy/local.settings.json');\"", + "sync-local-settings": "node scripts/sync-local-settings.mjs", "azurite": "node start-azurite.mjs" }, "dependencies": { diff --git a/apps/api/scripts/sync-local-settings.mjs b/apps/api/scripts/sync-local-settings.mjs new file mode 100644 index 000000000..57b89d37a --- /dev/null +++ b/apps/api/scripts/sync-local-settings.mjs @@ -0,0 +1,59 @@ +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { buildPortlessUrl, getHostnames } from '../../../scripts/local-dev/portless-hostnames.mjs'; +import { getAzuriteConnectionString } from '../../../scripts/local-dev/worktree-ports.mjs'; + +const apiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const mode = process.argv[2]; +const localSettingsPath = path.join(apiDir, 'local.settings.json'); +const e2eLocalSettingsPath = path.join(apiDir, 'local-settings.e2e.json'); +const targetPath = path.join(apiDir, 'deploy', 'local.settings.json'); + +mkdirSync(path.dirname(targetPath), { recursive: true }); + +if (!mode) { + if (existsSync(localSettingsPath)) { + copyFileSync(localSettingsPath, targetPath); + } + process.exit(0); +} + +if (mode !== 'e2e') { + throw new Error('[sync-local-settings] Invalid mode: expected one of e2e'); +} + +if (!existsSync(e2eLocalSettingsPath)) { + throw new Error(`[sync-local-settings] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`); +} + +const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf-8')); +applyE2EOverrides(settings); +writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`); + +function applyE2EOverrides(settings) { + const values = { ...(settings.Values ?? {}) }; + + // Worktree-scoped overrides: when WORKTREE_NAME is set the proxy hostnames + // and Azurite ports are scoped to that worktree, so we rewrite the URLs and + // connection strings here. Without WORKTREE_NAME the committed JSON values + // already match the default hostnames, so we leave them alone. + if (process.env.WORKTREE_NAME) { + const hostnames = getHostnames(); + values.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community'); + values.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); + values.STAFF_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/staff'); + values.STAFF_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); + const azurite = getAzuriteConnectionString(values); + values.AZURE_STORAGE_CONNECTION_STRING = azurite; + values.AzureWebJobsStorage = azurite; + } + + // Runtime-only injection: the e2e harness spawns MongoMemoryServer on a + // random port and passes the connection string through process.env. + if (process.env.COSMOSDB_CONNECTION_STRING) { + values.COSMOSDB_CONNECTION_STRING = process.env.COSMOSDB_CONNECTION_STRING; + } + + settings.Values = values; +} diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index ac3dff4e4..cc253db13 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -1,28 +1,14 @@ import { spawn } from 'node:child_process'; -import net from 'node:net'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { getAzuritePorts } from '../../build-pipeline/scripts/worktree-ports.mjs'; +import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { isPortListening, waitForPort } from '../../scripts/local-dev/port-ready.mjs'; +import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs'; const ports = getAzuritePorts(); const worktreeName = process.env.WORKTREE_NAME ?? ''; const storageSuffix = worktreeName ? `-${worktreeName}` : ''; -function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - if (await isPortListening(ports.blob)) { - console.log(`[azurite] already running (blob port ${ports.blob}), skipping`); + console.log(`[azurite] ready (blob port ${ports.blob})`); process.exit(0); } @@ -49,6 +35,14 @@ for (const proc of procs) { }); } +if (await waitForPort(ports.blob, { timeoutMs: 30_000 })) { + console.log(`[azurite] ready (blob port ${ports.blob})`); +} else { + console.error(`[azurite] blob port ${ports.blob} did not become ready within 30000ms`); + for (const p of procs) p.kill(); + process.exit(1); +} + process.on('SIGINT', () => { for (const p of procs) p.kill('SIGINT'); }); diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs index 9dc871786..22b30e1c7 100644 --- a/apps/api/start-dev.mjs +++ b/apps/api/start-dev.mjs @@ -1,9 +1,9 @@ import { spawn } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; -import { getAzuriteConnectionString, getMongoConnectionString } from '../../build-pipeline/scripts/worktree-ports.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; +import { getAzuriteConnectionString, getMongoConnectionString } from '../../scripts/local-dev/worktree-ports.mjs'; const envPort = process.env.PORT; @@ -22,8 +22,7 @@ const childEnv = { // Only inject worktree-scoped overrides when running in worktree mode. // When WORKTREE_NAME is absent, local.settings.json remains the source of truth. -// Use `??=` so callers (e.g. e2e harness with a MongoMemoryServer port) can override -// any individual value via process.env before invoking this script. +// Use `??=` so callers can override any individual value via process.env. if (process.env.WORKTREE_NAME) { const hostnames = getHostnames(); childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community'); @@ -42,11 +41,4 @@ const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/' env: childEnv, }); -child.on('exit', (code, signal) => { - // Turbo sends signals to interrupt persistent tasks; treat those as graceful exits. - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 6487a447c..75ff6794b 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -4,7 +4,7 @@ "build": { "cache": true, "dependsOn": ["^build", "//#gen"], - "inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/build-pipeline/scripts/**"], + "inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/scripts/local-dev/**"], "outputs": ["$TURBO_EXTENDS$", "deploy/**"] }, "dev": { diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs index cb09162a8..8180032bb 100644 --- a/apps/docs/start-dev.mjs +++ b/apps/docs/start-dev.mjs @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; const port = process.env.PORT ?? '3001'; @@ -7,11 +7,4 @@ const child = spawn('docusaurus', ['start', '--port', port, '--host', '127.0.0.1 stdio: 'inherit', }); -child.on('exit', (code, signal) => { - // Turbo sends signals to interrupt persistent tasks; treat those as graceful exits. - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 1b39ae1ef..8d15a147e 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,22 +1,8 @@ -import net from 'node:net'; -import { getMongoPort } from '../../build-pipeline/scripts/worktree-ports.mjs'; +import { isPortListening } from '../../scripts/local-dev/port-ready.mjs'; +import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; const MONGO_PORT = getMongoPort(); -function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - if (await isPortListening(MONGO_PORT)) { console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); process.exit(0); diff --git a/apps/server-oauth2-mock/start-dev.mjs b/apps/server-oauth2-mock/start-dev.mjs index 2bd4f4b1e..3f9d0f985 100644 --- a/apps/server-oauth2-mock/start-dev.mjs +++ b/apps/server-oauth2-mock/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -17,10 +17,4 @@ const child = spawn('tsx', ['src/index.ts'], { env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/ui-community/.env.e2e b/apps/ui-community/.env.e2e new file mode 100644 index 000000000..35b7f837a --- /dev/null +++ b/apps/ui-community/.env.e2e @@ -0,0 +1,9 @@ +# E2E defaults. Vite loads this via `vite --mode e2e`; +# process env supplied by local shells or CI still has precedence. +NODE_ENV=development +VITE_APP_UI_COMMUNITY_B2C_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/community +VITE_APP_UI_COMMUNITY_B2C_CLIENTID=mock-client +VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI=https://ownercommunity.localhost:1355/auth-redirect +VITE_APP_UI_COMMUNITY_B2C_SCOPES=openid +VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql +VITE_APP_UI_COMMUNITY_BASE_URL=https://ownercommunity.localhost:1355 diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 5226fb736..62c79117f 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -13,15 +13,15 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { +const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +if (viteMode) { + viteArgs.push('--mode', viteMode); +} + +const child = spawn('vite', viteArgs, { stdio: 'inherit', env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index b7c32402e..e49eac493 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -5,13 +5,13 @@ "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] }, "dev:worktree": { "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] } } } diff --git a/apps/ui-staff/.env.e2e b/apps/ui-staff/.env.e2e new file mode 100644 index 000000000..4eec69940 --- /dev/null +++ b/apps/ui-staff/.env.e2e @@ -0,0 +1,8 @@ +# E2E defaults. Vite loads this via `vite --mode e2e`; +# process env supplied by local shells or CI still has precedence. +NODE_ENV=development +VITE_APP_UI_STAFF_AAD_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/staff +VITE_APP_UI_STAFF_AAD_CLIENTID=mock-client +VITE_APP_UI_STAFF_AAD_REDIRECT_URI=https://staff.ownercommunity.localhost:1355/auth-redirect +VITE_APP_UI_STAFF_AAD_SCOPES=openid +VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index eaa17cdb8..0ef5c7673 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../build-pipeline/scripts/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../build-pipeline/scripts/portless-hostnames.mjs'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; +import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; const childEnv = { ...process.env }; @@ -11,15 +11,15 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const child = spawn('vite', ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1'], { +const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +if (viteMode) { + viteArgs.push('--mode', viteMode); +} + +const child = spawn('vite', viteArgs, { stdio: 'inherit', env: childEnv, }); -child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; -}); +forwardChildExit(child); diff --git a/biome.json b/biome.json index 2ed46c509..e14913384 100644 --- a/biome.json +++ b/biome.json @@ -166,7 +166,7 @@ }, { "includes": ["**/src/**/*.ts"], "javascript": { "globals": [] } }, { - "includes": ["build-pipeline/scripts/**/*.{c|m}js"], + "includes": ["build-pipeline/scripts/**/*.{c|m}js", "scripts/local-dev/**/*.mjs"], "linter": { "rules": { "style": { "noCommonJs": "off" } } } }, { diff --git a/build-pipeline/scripts/dev-process-exit.mjs b/build-pipeline/scripts/dev-process-exit.mjs deleted file mode 100644 index 04eabd5a4..000000000 --- a/build-pipeline/scripts/dev-process-exit.mjs +++ /dev/null @@ -1,8 +0,0 @@ -const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); -const INTERRUPT_EXIT_CODES = new Set([130, 143]); - -export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); - -export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(code); - -export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); diff --git a/build-pipeline/scripts/portless-hostnames.d.mts b/build-pipeline/scripts/portless-hostnames.d.mts deleted file mode 100644 index 139d75e94..000000000 --- a/build-pipeline/scripts/portless-hostnames.d.mts +++ /dev/null @@ -1,13 +0,0 @@ -export const PORTLESS_PORT: number; - -export interface PortlessHostnames { - uiCommunity: string; - uiStaff: string; - api: string; - mockAuth: string; - docs: string; -} - -export function getHostnames(): PortlessHostnames; - -export function buildPortlessUrl(hostname: string, path?: string): string; diff --git a/build-pipeline/scripts/worktree-ports.d.mts b/build-pipeline/scripts/worktree-ports.d.mts deleted file mode 100644 index c2cbdf86e..000000000 --- a/build-pipeline/scripts/worktree-ports.d.mts +++ /dev/null @@ -1,5 +0,0 @@ -export function getWorktreePortOffset(): number; -export function getMongoPort(): number; -export function getAzuritePorts(): { blob: number; queue: number; table: number }; -export function getAzuriteConnectionString(): string; -export function getMongoConnectionString(): string; diff --git a/knip.json b/knip.json index 4a560cc9a..130ffcb7f 100644 --- a/knip.json +++ b/knip.json @@ -98,7 +98,21 @@ } }, "ignoreWorkspaces": ["packages/cellix/config-typescript"], - "ignore": ["build-pipeline/scripts/**", "**/fixtures/**", "**/*.test.ts", "**/*.spec.ts", "**/*.stories.tsx", "**/dist/**", "**/coverage/**", "**/__tests__/**", "**/tests/**", ".agents/**", ".github/**", "portless.config.cjs"], + "ignore": [ + "build-pipeline/scripts/**", + "scripts/local-dev/**", + "**/fixtures/**", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.stories.tsx", + "**/dist/**", + "**/coverage/**", + "**/__tests__/**", + "**/tests/**", + ".agents/**", + ".github/**", + "portless.config.cjs" + ], "ignoreIssues": { "codegen.yml": ["unlisted"] }, diff --git a/package.json b/package.json index 3dc82ec64..46e1f3dd3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", "check-sonar": "node build-pipeline/scripts/check-sonar-quality-gate.cjs", - "verify": "pnpm run format:check && pnpm run test:arch && pnpm run test:coverage:merge && pnpm run knip && pnpm run audit && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", + "verify": "pnpm run format:check && pnpm run test:arch && pnpm run test:coverage:merge && pnpm run test:e2e:worktree && pnpm run knip && pnpm run audit && pnpm run snyk && pnpm run sonar:pr && pnpm run check-sonar", "knip": "knip", "snyk": "pnpm run snyk:test && pnpm run snyk:code", "snyk:report": "pnpm run snyk:monitor && pnpm run snyk:code:report", diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts index 7e774dc2c..ca8096e34 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts @@ -33,7 +33,7 @@ function createNoOpApolloServerService(): ServiceApolloServer; + } as unknown as ServiceApolloServer>; } export function createMockApplicationServicesFactory(serviceMongoose: ServiceMongoose): ApplicationServicesFactory { diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts index cdc91878a..bbcfd73b0 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts @@ -1,10 +1,8 @@ import { GraphQLTestServer, MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; -import { apiSettings } from '@ocom-verification/verification-shared/settings'; import { createMockApplicationServicesFactory } from './application-services/index.ts'; // Shared infrastructure — persists across scenarios within a single test run let mongoDBServer: MongoDBTestServer | undefined; -let mongoSeeded = false; let graphQLServer: GraphQLTestServer | undefined; let apiUrl: string | undefined; @@ -26,27 +24,13 @@ export async function stopAll(): Promise { mongoDBServer = undefined; } apiUrl = undefined; - mongoSeeded = false; } -async function ensureMongoDBServer(options?: { port?: number; dbName?: string }): Promise { +async function ensureMongoDBServer(): Promise { if (mongoDBServer) return mongoDBServer; - const connectionString = options?.port ? apiSettings.cosmosDbConnectionString : ''; - - if (connectionString && (await MongoDBTestServer.isReachable(connectionString))) { - if (!mongoSeeded) { - await MongoDBTestServer.seedData(connectionString, options?.dbName ?? apiSettings.cosmosDbName); - mongoSeeded = true; - } - mongoDBServer = new MongoDBTestServer(); - await mongoDBServer.start(options); - return mongoDBServer; - } - mongoDBServer = new MongoDBTestServer(); - await mongoDBServer.start(options); - mongoSeeded = true; + await mongoDBServer.start(); return mongoDBServer; } diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts index 349b3edee..5fc3e8f2f 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts @@ -11,7 +11,7 @@ const dom = new JSDOM('
', { url: 'http://localhost:3000', pretendToBeVisual: true, }); -const domGlobal = (dom as unknown as Record)['window']; +const domGlobal = dom['window'] as unknown as Window & typeof globalThis; // biome-ignore lint/suspicious/noExplicitAny: attaching browser globals requires dynamic property assignment const g = globalThis as any; diff --git a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs index 827e78164..2097f480c 100644 --- a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs +++ b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs @@ -8,6 +8,7 @@ const SKIP_DIRS = new Set(['node_modules', '.turbo', 'build-artifacts', 'dist', // Single source of truth for known portal identifiers (ADR-0031). // Keep this list in sync with apps/docs/docs/portals/PORTAL_REGISTRY.md. const CANONICAL_PORTALS = ['UI_COMMUNITY', 'UI_STAFF']; +const VITE_RESERVED_VARS = new Set(['VITE_MODE']); function walkDir(dir, fileList = []) { const files = fs.readdirSync(dir, { withFileTypes: true }); @@ -319,6 +320,16 @@ function validateEnvNames(options = {}) { for (const m of matches) { const variable = m.match; const relPath = `${path.relative(rootDir, filePath)}:${getLineNumber(newlineOffsets, m.index)}`; + if (VITE_RESERVED_VARS.has(variable)) { + results.push({ + variable, + status: 'compliant', + portal: 'BUILD', + ownerGroup: 'ocm-platform', + location: relPath, + }); + continue; + } if (!variable.startsWith('VITE_APP_') && !variable.startsWith('VITE_COMMON_')) { results.push({ variable, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts new file mode 100644 index 000000000..b0ebf360f --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts @@ -0,0 +1,9 @@ +import { fileURLToPath } from 'node:url'; + +const workspaceRootUrl = new URL('../../../../../../../', import.meta.url); + +export const appPaths = { + apiDir: fileURLToPath(new URL('apps/api/', workspaceRootUrl)), + oauth2MockDir: fileURLToPath(new URL('apps/server-oauth2-mock/', workspaceRootUrl)), + uiCommunityDir: fileURLToPath(new URL('apps/ui-community/', workspaceRootUrl)), +} as const; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts new file mode 100644 index 000000000..51e02949c --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts @@ -0,0 +1,14 @@ +/** + * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. + * + * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript + * source is executed directly. Child processes spawned by the test harness + * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on + * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate + * crash. Callers can spread additional overrides on top of the result. + */ +export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { + const env = { ...process.env, ...overrides }; + delete env['NODE_OPTIONS']; + return env; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts deleted file mode 100644 index 6680d32a8..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/e2e-defaults.ts +++ /dev/null @@ -1,65 +0,0 @@ -// biome-ignore format: keep the public Azurite dev key split across chunks for static analysis. - -/** - * Non-secret environment defaults for the e2e harness. - * - * Used when `apps/api/local.settings.json` is absent (typical in CI). Every - * value here is either a public Azurite development credential or a mock-only - * constant — none of them are valid in any real environment, and they are - * never applied outside the e2e harness. - * - * Worktree-scoped values (OIDC URLs, mongo/azurite ports, connection strings) - * are computed at runtime by `build-pipeline/scripts/portless-hostnames.mjs` - * and `worktree-ports.mjs`; they intentionally do NOT live here. - */ - -const AZURITE_ACCOUNT_KEY = [ - 'Eby8vdM02xNOcqFlqUwJPLlm', - 'EtlCDXJ1OUzFT50uSRZ6IFs', - 'uFq2UVErCz4I6tq/K1SZFP', - 'TOtr/KBHBeksoGMGw==', -].join(''); - -const E2E_API_DEFAULTS = { - FUNCTIONS_WORKER_RUNTIME: 'node', - NODE_ENV: 'development', - CONFIG_VERSION: '3.0', - - ACCOUNT_PORTAL_OIDC_AUDIENCE: 'mock-client', - ACCOUNT_PORTAL_OIDC_IGNORE_ISSUER: 'true', - STAFF_PORTAL_OIDC_AUDIENCE: 'mock-client', - STAFF_PORTAL_OIDC_IGNORE_ISSUER: 'true', - - COSMOSDB_DBNAME: 'owner-community', - - // Well-known Azurite dev account — documented in Azurite's README, not a secret. - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: AZURITE_ACCOUNT_KEY, -} as const; - -/** - * Apply e2e defaults to `process.env` for any key not already set. A pre-existing - * env var (from the developer's shell or a copied local.settings.json) always wins. - * Spawned child processes inherit `process.env`, so this propagates to azurite, - * the api function host, and the oauth2 mock without any per-server plumbing. - */ -export function applyE2EDefaultsToEnv(): void { - for (const [key, value] of Object.entries(E2E_API_DEFAULTS)) { - process.env[key] ??= value; - } -} - -/** - * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. - * - * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript - * source is executed directly. Child processes spawned by the test harness - * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on - * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate - * crash. Callers can spread additional overrides on top of the result. - */ -export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { - const env = { ...process.env, ...overrides }; - delete env['NODE_OPTIONS']; - return env; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index 6ff6ace04..a4dcfaa4b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -1,7 +1,7 @@ import { type ChildProcess, spawn } from 'node:child_process'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { spawnEnv } from './e2e-defaults.ts'; +import { spawnEnv } from './child-process-env.ts'; import { getPortlessPath } from './resolve-portless.ts'; /** diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index a92d3f893..3a45a7176 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -1,8 +1,4 @@ -import { execFileSync } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { apiSettings } from '@ocom-verification/verification-shared/settings'; -import { spawnEnv } from './e2e-defaults.ts'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { buildUrl, getHostnames, getMongoConnectionString } from './test-environment.ts'; @@ -10,38 +6,10 @@ const hostnames = getHostnames(); /** * Spawns the api dev server the same way `pnpm dev:worktree` does. The - * worktree-aware overrides (OIDC URLs, Azurite connection, etc.) all come - * from apps/api/start-dev.mjs — we only inject the dynamic MongoDB - * connection string from MongoMemoryServer here. + * Azure Functions runtime reads deploy/local.settings.json, which is generated + * from the committed e2e local settings plus runtime-only test values. */ export class TestApiServer extends PortlessServer { - override async start(): Promise { - // Mirror the `predev:worktree` lifecycle hook so deploy/ and local.settings.json - // stay in sync — start-dev.mjs is invoked directly here, bypassing pnpm's pre-hook. - execFileSync('pnpm', ['run', 'predev:worktree'], { - cwd: this.cwd, - env: spawnEnv(), - stdio: 'pipe', - }); - - // Patch deploy/local.settings.json with e2e-specific values. - // Azure Functions loads local.settings.json values into the worker env, - // overriding any parent-process env vars. We must patch the file so the - // worker gets the MongoMemoryServer connection string (random port) and - // the inspector is disabled (port 5858 may already be in use). - const settingsPath = join(this.cwd, 'deploy', 'local.settings.json'); - try { - const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - settings.Values.COSMOSDB_CONNECTION_STRING = getMongoConnectionString(); - settings.Values['languageWorkers__node__arguments'] = ''; - writeFileSync(settingsPath, JSON.stringify(settings, null, '\t')); - } catch { - /* best-effort — file may not exist in CI */ - } - - await super.start(); - } - protected get probeUrl() { return buildUrl(hostnames.api, '/api/graphql'); } @@ -70,15 +38,17 @@ export class TestApiServer extends PortlessServer { protected get serverName() { return 'TestApiServer'; } + protected override get executable() { + return 'pnpm'; + } protected get spawnArgs() { - return [hostnames.api, 'node', 'start-dev.mjs']; + return ['run', 'dev:e2e']; } protected get cwd() { - return apiSettings.apiDir; + return appPaths.apiDir; } protected override get extraEnv() { - // start-dev.mjs handles the rest via WORKTREE_NAME + `??=` fallbacks. return { COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), }; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index d43d9efd3..47558526a 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -1,10 +1,24 @@ import { type ChildProcess, spawn } from 'node:child_process'; -import net from 'node:net'; import { join } from 'node:path'; import type { TestServer } from '@ocom-verification/verification-shared/servers'; -import { apiSettings, getTimeout } from '@ocom-verification/verification-shared/settings'; -import { getAzuritePorts } from '../../../../../../../build-pipeline/scripts/worktree-ports.mjs'; -import { spawnEnv } from './e2e-defaults.ts'; +import { getTimeout } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; +import { spawnEnv } from './child-process-env.ts'; + +interface PortReadyModule { + isPortListening(port: number): Promise; + waitForPort(port: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise; +} + +interface WorktreePortsModule { + getAzuritePorts(): { blob: number; queue: number; table: number }; +} + +const portReadyModuleUrl = new URL('../../../../../../../scripts/local-dev/port-ready.mjs', import.meta.url).href; +const worktreePortsModuleUrl = new URL('../../../../../../../scripts/local-dev/worktree-ports.mjs', import.meta.url).href; + +const { isPortListening, waitForPort } = (await import(portReadyModuleUrl)) as PortReadyModule; +const { getAzuritePorts } = (await import(worktreePortsModuleUrl)) as WorktreePortsModule; /** * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits @@ -24,10 +38,10 @@ export class TestAzuriteServer implements TestServer { if (this.process || this.startedByUs) return; if (await isPortListening(this.blobPort)) return; - const binDir = join(apiSettings.apiDir, 'node_modules', '.bin'); + const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); this.process = spawn('node', ['start-azurite.mjs'], { - cwd: apiSettings.apiDir, + cwd: appPaths.apiDir, env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], @@ -68,30 +82,16 @@ export class TestAzuriteServer implements TestServer { } private async waitForReady(): Promise { - const deadline = Date.now() + getTimeout('serverStartup'); - const interval = getTimeout('healthProbeInterval'); - while (Date.now() < deadline) { - if (await isPortListening(this.blobPort)) return; - await new Promise((resolve) => setTimeout(resolve, interval)); + const ready = await waitForPort(this.blobPort, { + timeoutMs: getTimeout('serverStartup'), + intervalMs: getTimeout('healthProbeInterval'), + }); + if (!ready) { + throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); } - throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); } } -function isPortListening(port: number): Promise { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - function killProcess(proc: ChildProcess, signal: NodeJS.Signals, useGroup: boolean): void { if (useGroup && proc.pid) { try { diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 8de00bfca..2251dba3c 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -1,4 +1,4 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { buildUrl, getHostnames } from './test-environment.ts'; @@ -25,12 +25,13 @@ export class TestCommunityViteServer extends PortlessServer { return [hostnames.uiCommunity, 'node', 'start-dev.mjs']; } protected get cwd() { - return apiSettings.uiCommunityDir; + return appPaths.uiCommunityDir; } protected override get extraEnv() { return { BROWSER: 'none', NODE_ENV: 'development', + VITE_MODE: 'e2e', }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index 6550beb7f..d9fa97a66 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,6 +1,5 @@ import { execFileSync } from 'node:child_process'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; -import { applyE2EDefaultsToEnv } from './e2e-defaults.ts'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; @@ -16,16 +15,13 @@ export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community') export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; /** - * Apply e2e env defaults (so CI runs without local.settings.json) and ensure - * the portless proxy is running. The `proxy start` invocation is idempotent — + * Ensure the portless proxy is running. The `proxy start` invocation is idempotent, * if another worktree or dev session already started it on the shared port, * this returns immediately. */ export function initTestEnvironment() { if (proxyInitialized) return; - applyE2EDefaultsToEnv(); - execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { timeout: 15_000, stdio: 'pipe', diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index 7d843f446..d21f0e090 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,4 +1,4 @@ -import { apiSettings } from '@ocom-verification/verification-shared/settings'; +import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; @@ -24,7 +24,7 @@ export class TestOAuth2Server extends PortlessServer { return [hostnames.mockAuth, 'node', 'start-dev.mjs']; } protected get cwd() { - return apiSettings.oauth2MockDir; + return appPaths.oauth2MockDir; } getUrl(): string { diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index 4a199c38c..a5adb33a1 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -3,12 +3,36 @@ "tasks": { "test:e2e": { "dependsOn": ["^build"], - "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], + "inputs": [ + "src/**/*.ts", + "config/**", + "$TURBO_ROOT$/apps/api/local-settings.e2e.json", + "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", + "$TURBO_ROOT$/apps/ui-community/.env", + "$TURBO_ROOT$/apps/ui-community/.env.e2e", + "$TURBO_ROOT$/apps/ui-staff/.env", + "$TURBO_ROOT$/apps/ui-staff/.env.e2e", + "$TURBO_ROOT$/scripts/local-dev/**", + "cucumber.js", + "package.json" + ], "cache": false }, "test:serenity": { "dependsOn": ["^build"], - "inputs": ["src/**/*.ts", "cucumber.js", "package.json"], + "inputs": [ + "src/**/*.ts", + "config/**", + "$TURBO_ROOT$/apps/api/local-settings.e2e.json", + "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", + "$TURBO_ROOT$/apps/ui-community/.env", + "$TURBO_ROOT$/apps/ui-community/.env.e2e", + "$TURBO_ROOT$/apps/ui-staff/.env", + "$TURBO_ROOT$/apps/ui-staff/.env.e2e", + "$TURBO_ROOT$/scripts/local-dev/**", + "cucumber.js", + "package.json" + ], "cache": false } } diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts index 68ac69337..5969fa475 100644 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ b/packages/ocom-verification/verification-shared/src/settings/index.ts @@ -1,11 +1,2 @@ -export { apiSettings, uiSettings } from './local-settings.ts'; export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from './portless-settings.ts'; -export { - findWorkspaceRoot, - readDotEnv, - readJsonSettings, - readSetting, - requireSetting, - resolveWorkspacePath, -} from './settings-utils.ts'; export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts b/packages/ocom-verification/verification-shared/src/settings/local-settings.ts deleted file mode 100644 index 8e9929f78..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/local-settings.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'node:path'; -import { findWorkspaceRoot, readDotEnv, readJsonSettings, readSetting, requireSetting, resolveWorkspacePath } from './settings-utils.ts'; - -const workspaceRoot = findWorkspaceRoot(); -const apiSettingsPath = resolveWorkspacePath(workspaceRoot, 'apps/api/local.settings.json'); -const uiEnvPath = resolveWorkspacePath(workspaceRoot, 'apps/ui-community/.env'); - -const apiValues = readJsonSettings(apiSettingsPath); -const uiValues = readDotEnv(uiEnvPath); - -export const apiSettings = { - cosmosDbConnectionString: readSetting(apiValues, 'COSMOSDB_CONNECTION_STRING', '') ?? '', - cosmosDbName: readSetting(apiValues, 'COSMOSDB_DBNAME', 'owner-community') ?? 'owner-community', - - apiDir: path.dirname(apiSettingsPath), - oauth2MockDir: path.join(workspaceRoot, 'apps', 'server-oauth2-mock'), - uiCommunityDir: path.dirname(uiEnvPath), -} as const; - -export const uiSettings = { - baseUrl: requireSetting(uiValues, 'VITE_APP_UI_COMMUNITY_BASE_URL', 'VITE_APP_UI_COMMUNITY_BASE_URL is required in .env'), - - graphqlEndpoint: requireSetting(uiValues, 'VITE_COMMON_API_ENDPOINT', 'VITE_COMMON_API_ENDPOINT is required in .env'), -} as const; diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index 405f1802e..a77b96e19 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,7 +1,25 @@ /** - * Re-export of the canonical portless hostname helpers used by both the + * Runtime bridge to the canonical portless hostname helpers used by both the * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as * the single source of truth means there is exactly one place that derives * hostnames from .env and applies the WORKTREE_NAME suffix. */ -export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from '../../../../../build-pipeline/scripts/portless-hostnames.mjs'; +interface PortlessHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +interface PortlessHostnamesModule { + PORTLESS_PORT: number; + getHostnames(): PortlessHostnames; + buildPortlessUrl(hostname: string, path?: string): string; +} + +const portlessHostnamesModuleUrl = new URL('../../../../../scripts/local-dev/portless-hostnames.mjs', import.meta.url).href; + +const { buildPortlessUrl, getHostnames, PORTLESS_PORT } = (await import(portlessHostnamesModuleUrl)) as PortlessHostnamesModule; + +export { buildPortlessUrl, getHostnames, PORTLESS_PORT }; diff --git a/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts b/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts deleted file mode 100644 index c51a891eb..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/settings-utils.ts +++ /dev/null @@ -1,93 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const defaultStartDir = fileURLToPath(new URL('.', import.meta.url)); - -/** - * Walks up from `startDir` looking for pnpm-workspace.yaml to locate the - * monorepo root. Projects that use a different workspace marker should pass a - * custom `markerFile`. - */ -export function findWorkspaceRoot(startDir = defaultStartDir, markerFile = 'pnpm-workspace.yaml'): string { - let dir = startDir; - - while (dir !== path.dirname(dir)) { - if (fs.existsSync(path.join(dir, markerFile))) { - return dir; - } - dir = path.dirname(dir); - } - - throw new Error(`Could not find workspace root (${markerFile})`); -} - -/** - * Reads an Azure Functions-style settings file ({ "Values": { ... } }) and - * returns the Values map. Returns `{}` if the file doesn't exist. - */ -export function readJsonSettings(filePath: string): Record { - if (!fs.existsSync(filePath)) { - return {}; - } - const raw = fs.readFileSync(filePath, 'utf-8'); - const parsed = JSON.parse(raw) as { Values?: Record }; - return parsed.Values ?? {}; -} - -/** - * Reads a simple `KEY=VALUE` .env file. Ignores blank lines and `#` comments. - * Returns `{}` if the file doesn't exist. - */ -export function readDotEnv(filePath: string): Record { - if (!fs.existsSync(filePath)) { - return {}; - } - - const lines = fs.readFileSync(filePath, 'utf-8').split('\n'); - const result: Record = {}; - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { - continue; - } - - const eqIndex = trimmed.indexOf('='); - if (eqIndex === -1) { - continue; - } - - result[trimmed.slice(0, eqIndex)] = trimmed.slice(eqIndex + 1); - } - - return result; -} - -/** - * Resolves a target path against the workspace root unless it's already - * absolute. - */ -export function resolveWorkspacePath(workspaceRoot: string, targetPath: string): string { - return path.isAbsolute(targetPath) ? targetPath : path.join(workspaceRoot, targetPath); -} - -/** - * Returns the value for `key`, or `defaultValue` if absent/empty. Use this - * when an optional setting has a reasonable default. - */ -export function readSetting(values: Record, key: string, defaultValue?: string): string | undefined { - return values[key] ?? defaultValue; -} - -/** - * Returns the value for `key`. Throws `errorMessage` if the value is missing - * or empty. Use this when a setting is required for the test run to work. - */ -export function requireSetting(values: Record, key: string, errorMessage: string): string { - const value = values[key]; - if (value) { - return value; - } - throw new Error(errorMessage); -} diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index 0fe4c8bd5..18fb61910 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -35,7 +35,7 @@ "@opentelemetry/instrumentation-mongoose": "0.47.0", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", - "@opentelemetry/sdk-node": "0.217.0", + "@opentelemetry/sdk-node": "0.57.2", "@opentelemetry/sdk-trace-node": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b33c6e41..929e94059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,8 @@ overrides: ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 + shell-quote@<1.8.4: 1.8.4 + '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 patchedDependencies: '@azure/functions@4.11.0': 69772ce521bf6df67d814ff4f419f19b5e966a41c4ce80b5938143ad628e5645 @@ -948,25 +950,25 @@ importers: version: link:../config-vitest '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-a11y': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-docs': specifier: ^9.1.3 - version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-onboarding': specifier: ^9.1.3 - version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) '@storybook/react': specifier: ^9.1.9 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) '@storybook/react-vite': specifier: ^9.1.3 - version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -978,10 +980,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitest/browser': specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/browser-playwright': specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.2(vitest@4.1.2) @@ -999,13 +1001,13 @@ importers: version: 6.0.1 storybook: specifier: 'catalog:' - version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1701,8 +1703,8 @@ importers: specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: 0.217.0 - version: 0.217.0(@opentelemetry/api@1.9.0) + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': specifier: 1.30.1 version: 1.30.1(@opentelemetry/api@1.9.0) @@ -4912,10 +4914,6 @@ packages: resolution: {integrity: sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.217.0': - resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -4928,24 +4926,12 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.217.0': - resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@1.30.1': resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/context-async-hooks@2.7.1': - resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@1.25.1': resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} engines: {node: '>=14'} @@ -4970,39 +4956,39 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': - resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.217.0': - resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-http@0.57.2': + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.217.0': - resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-logs-otlp-proto@0.57.2': + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': - resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.217.0': - resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': - resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5012,27 +4998,27 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': - resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.217.0': - resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-http@0.57.2': + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.217.0': - resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-trace-otlp-proto@0.57.2': + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.7.1': - resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/exporter-zipkin@1.30.1': + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -5072,33 +5058,33 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.217.0': - resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.52.1': - resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.217.0': - resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.217.0': - resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-grpc-exporter-base@0.57.2': + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.217.0': - resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5108,24 +5094,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-b3@2.7.1': - resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@1.30.1': resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.7.1': - resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@1.30.1': resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} engines: {node: '>=14'} @@ -5138,12 +5112,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.217.0': - resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-logs@0.57.2': resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} engines: {node: '>=14'} @@ -5162,9 +5130,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.217.0': - resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/sdk-node@0.57.2': + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -5186,12 +5154,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.7.1': - resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace-web@2.7.1': resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -9125,10 +9087,6 @@ packages: import-in-the-middle@2.0.6: resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} - import-in-the-middle@3.0.1: - resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} - engines: {node: '>=18'} - import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -11958,8 +11916,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} shimmer@1.2.1: @@ -14761,18 +14719,6 @@ snapshots: '@blazediff/core@1.9.1': {} - '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@neoconfetti/react': 1.0.0 - chromatic: 13.3.4 - filesize: 10.1.6 - jsonfile: 6.2.0 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - strip-ansi: 7.1.2 - transitivePeerDependencies: - - '@chromatic-com/cypress' - - '@chromatic-com/playwright' - '@chromatic-com/storybook@4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@neoconfetti/react': 1.0.0 @@ -15101,7 +15047,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -15137,7 +15083,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -15186,9 +15132,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': dependencies: - '@cucumber/messages': 32.3.1 + '@cucumber/messages': 32.2.0 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -16285,7 +16231,7 @@ snapshots: listr2: 4.0.5 log-symbols: 4.1.0 micromatch: 4.0.8 - shell-quote: 1.8.3 + shell-quote: 1.8.4 string-env-interpolation: 1.0.1 ts-log: 2.2.7 tslib: 2.8.1 @@ -16808,15 +16754,6 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - glob: 10.5.0 - magic-string: 0.30.21 - react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - optionalDependencies: - typescript: 6.0.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 10.5.0 @@ -16969,10 +16906,6 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.217.0': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -16983,20 +16916,10 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - yaml: 2.8.3 - '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17017,66 +16940,66 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17086,42 +17009,42 @@ snapshots: '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 '@opentelemetry/instrumentation-dataloader@0.17.0(@opentelemetry/api@1.9.0)': dependencies: @@ -17176,19 +17099,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - import-in-the-middle: 3.0.1 - require-in-the-middle: 8.0.1 + '@opentelemetry/api-logs': 0.52.1 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.4 + shimmer: 1.2.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.52.1 + '@opentelemetry/api-logs': 0.57.2 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 @@ -17197,29 +17123,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) protobufjs: 7.5.8 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': @@ -17227,21 +17153,11 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17254,14 +17170,6 @@ snapshots: '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17281,34 +17189,29 @@ snapshots: '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.217.0 - '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 transitivePeerDependencies: - supports-color @@ -17336,13 +17239,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) semver: 7.7.4 - '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18300,31 +18196,12 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@storybook/global': 5.0.0 - axe-core: 4.11.0 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-a11y@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@storybook/global': 5.0.0 axe-core: 4.11.0 storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) - '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - '@storybook/addon-docs@9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -18338,10 +18215,6 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-onboarding@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18362,22 +18235,6 @@ snapshots: - react - react-dom - '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - prompts: 2.4.2 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/runner': 4.1.2 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - react - - react-dom - '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': dependencies: '@storybook/global': 5.0.0 @@ -18394,13 +18251,6 @@ snapshots: - react - react-dom - '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ts-dedent: 2.2.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@storybook/builder-vite@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@storybook/csf-plugin': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -18408,11 +18258,6 @@ snapshots: ts-dedent: 2.2.0 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - unplugin: 1.16.1 - '@storybook/csf-plugin@9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18425,38 +18270,12 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': - dependencies: - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react-dom-shim@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/react': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) - find-up: 7.0.0 - magic-string: 0.30.21 - react: 19.2.0 - react-docgen: 8.0.2 - react-dom: 19.2.0(react@19.2.0) - resolve: 1.22.11 - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - tsconfig-paths: 4.2.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - transitivePeerDependencies: - - rollup - - supports-color - - typescript - '@storybook/react-vite@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.1(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -18477,16 +18296,6 @@ snapshots: - supports-color - typescript - '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - optionalDependencies: - typescript: 6.0.3 - '@storybook/react@9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 @@ -18990,19 +18799,6 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': - dependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - playwright: 1.59.0 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) @@ -19034,23 +18830,6 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': - dependencies: - '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.2 - magic-string: 0.30.21 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': dependencies: '@blazediff/core': 1.9.1 @@ -19080,7 +18859,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19101,14 +18880,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 @@ -19125,14 +18896,6 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.2 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -20250,7 +20013,7 @@ snapshots: dependencies: chalk: 4.1.2 rxjs: 7.8.2 - shell-quote: 1.8.3 + shell-quote: 1.8.4 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 @@ -21949,13 +21712,6 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 - import-in-the-middle@3.0.1: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - import-lazy@4.0.0: {} imurmurhash@0.1.4: {} @@ -22456,7 +22212,7 @@ snapshots: launch-editor@2.12.0: dependencies: picocolors: 1.1.1 - shell-quote: 1.8.3 + shell-quote: 1.8.4 less@4.4.2: dependencies: @@ -25256,7 +25012,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} shimmer@1.2.1: {} @@ -25466,28 +25222,6 @@ snapshots: graphql: 16.12.0 react: 19.2.0 - storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@storybook/global': 5.0.0 - '@testing-library/jest-dom': 6.9.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/spy': 3.2.4 - better-opn: 3.0.2 - esbuild: 0.25.12 - esbuild-register: 3.6.0(esbuild@0.25.12) - recast: 0.23.11 - semver: 7.7.4 - ws: 8.20.1 - transitivePeerDependencies: - - '@testing-library/dom' - - bufferutil - - msw - - supports-color - - utf-8-validate - - vite - storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@storybook/global': 5.0.0 @@ -26245,26 +25979,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.10 - rolldown: 1.0.0-rc.12(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.10.1 - esbuild: 0.25.12 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.4.2 - terser: 5.44.1 - tsx: 4.21.0 - yaml: 2.8.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -26315,36 +26029,6 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - tinyrainbow: 3.1.0 - vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.25.12)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - jsdom: 26.1.0 - transitivePeerDependencies: - - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 63e1e511e..5abbacdb1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -97,6 +97,8 @@ overrides: ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 + 'shell-quote@<1.8.4': 1.8.4 + '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 patchedDependencies: '@azure/functions@4.11.0': patches/@azure__functions@4.11.0.patch diff --git a/scripts/local-dev/dev-process-exit.mjs b/scripts/local-dev/dev-process-exit.mjs new file mode 100644 index 000000000..d762c4d5e --- /dev/null +++ b/scripts/local-dev/dev-process-exit.mjs @@ -0,0 +1,40 @@ +/** @typedef {import('node:child_process').ChildProcess} ChildProcess */ + +const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); +const INTERRUPT_EXIT_CODES = new Set([130, 143]); + +/** + * @param {NodeJS.Signals | null | undefined} signal + * @returns {boolean} + */ +export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); + +/** + * @param {number | null | undefined} code + * @returns {boolean} + */ +export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(/** @type {number} */ (code)); + +/** + * @param {NodeJS.Signals | null | undefined} signal + * @param {number | null | undefined} code + * @returns {boolean} + */ +export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); + +/** + * Wires a spawned dev child process to forward its exit status to the parent, + * treating Turbo's interrupt signals as graceful exits. Every `start-dev.mjs` + * runner ends with the same handler, so this is the single source of truth. + * @param {ChildProcess} child + * @returns {void} + */ +export function forwardChildExit(child) { + child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + process.exitCode = code ?? 1; + }); +} diff --git a/scripts/local-dev/port-ready.mjs b/scripts/local-dev/port-ready.mjs new file mode 100644 index 000000000..a24930f35 --- /dev/null +++ b/scripts/local-dev/port-ready.mjs @@ -0,0 +1,45 @@ +import net from 'node:net'; + +/** + * @typedef {object} WaitForPortOptions + * @property {number} [timeoutMs] + * @property {number} [intervalMs] + */ + +/** + * Resolves true if a TCP listener is accepting connections on the given port + * on 127.0.0.1. Used by the dev launchers to short-circuit when a service is + * already running (Azurite, Mongo) and by the e2e harness to wait for ports + * to come up. + * @param {number} port + * @returns {Promise} + */ +export function isPortListening(port) { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +/** + * Polls `isPortListening(port)` until it resolves true or the timeout elapses. + * Resolves true on success, false on timeout — callers decide how to react. + * @param {number} port + * @param {WaitForPortOptions} [options] + * @returns {Promise} + */ +export async function waitForPort(port, { timeoutMs = 30_000, intervalMs = 250 } = {}) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isPortListening(port)) return true; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return false; +} diff --git a/build-pipeline/scripts/portless-hostnames.mjs b/scripts/local-dev/portless-hostnames.mjs similarity index 81% rename from build-pipeline/scripts/portless-hostnames.mjs rename to scripts/local-dev/portless-hostnames.mjs index f5df73258..b2e9deb01 100644 --- a/build-pipeline/scripts/portless-hostnames.mjs +++ b/scripts/local-dev/portless-hostnames.mjs @@ -19,12 +19,27 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +/** + * @typedef {Record} DotEnvValues + * @typedef {object} PortlessHostnames + * @property {string} uiCommunity + * @property {string} uiStaff + * @property {string} api + * @property {string} mockAuth + * @property {string} docs + */ + const PORTLESS_PORT = 1355; const scriptDir = fileURLToPath(new URL('.', import.meta.url)); const workspaceRoot = resolve(scriptDir, '../..'); +/** + * @param {string} filePath + * @returns {DotEnvValues} + */ function readDotEnv(filePath) { if (!existsSync(filePath)) return {}; + /** @type {DotEnvValues} */ const result = {}; for (const line of readFileSync(filePath, 'utf-8').split('\n')) { const trimmed = line.trim(); @@ -36,6 +51,10 @@ function readDotEnv(filePath) { return result; } +/** + * @param {string} url + * @returns {string | null} + */ function hostnameFrom(url) { try { return new URL(url).hostname; @@ -44,7 +63,12 @@ function hostnameFrom(url) { } } -/** Splice `.` in before `.localhost` in an existing hostname. */ +/** + * Splice `.` in before `.localhost` in an existing hostname. + * @param {string} hostname + * @param {string} worktreeName + * @returns {string} + */ function applyWorktreeSuffix(hostname, worktreeName) { if (!worktreeName) return hostname; return hostname.replace('.localhost', `.${worktreeName}.localhost`); @@ -54,6 +78,7 @@ function applyWorktreeSuffix(hostname, worktreeName) { * Returns all service hostnames scoped to the current worktree (if any). * Hostname shapes are read from the tracked .env files — no names are * hardcoded in this module. + * @returns {PortlessHostnames} */ export function getHostnames() { const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); @@ -80,6 +105,9 @@ export function getHostnames() { /** * Builds a full portless-proxied URL for the given hostname and optional path. + * @param {string} hostname + * @param {string} [path] + * @returns {string} */ export function buildPortlessUrl(hostname, path = '') { return `https://${hostname}:${PORTLESS_PORT}${path}`; diff --git a/build-pipeline/scripts/worktree-ports.mjs b/scripts/local-dev/worktree-ports.mjs similarity index 77% rename from build-pipeline/scripts/worktree-ports.mjs rename to scripts/local-dev/worktree-ports.mjs index 9a86b183c..25d843cbf 100644 --- a/build-pipeline/scripts/worktree-ports.mjs +++ b/scripts/local-dev/worktree-ports.mjs @@ -2,6 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +/** + * @typedef {Record} SettingsValues + * @typedef {{ blob: number, queue: number, table: number }} AzuritePorts + */ + /** * Worktree-scoped port computation for service isolation. * @@ -19,17 +24,30 @@ import { fileURLToPath } from 'node:url'; const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const apiLocalSettingsPaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; +/** @type {SettingsValues | undefined} */ let apiLocalSettingsValues; -function getSetting(name) { - return process.env[name] ?? getApiLocalSetting(name); +/** + * @param {string} name + * @param {SettingsValues} [values] + * @returns {string | undefined} + */ +function getSetting(name, values) { + return process.env[name] ?? values?.[name] ?? getApiLocalSetting(name); } +/** + * @param {string} name + * @returns {string | undefined} + */ function getApiLocalSetting(name) { apiLocalSettingsValues ??= readApiLocalSettingsValues(); return apiLocalSettingsValues[name]; } +/** + * @returns {SettingsValues} + */ function readApiLocalSettingsValues() { for (const settingsPath of apiLocalSettingsPaths) { if (!fs.existsSync(settingsPath)) continue; @@ -42,6 +60,7 @@ function readApiLocalSettingsValues() { /** * Returns a deterministic port offset in the range [100, 4900] (step 100) * for the current worktree. Returns 0 when WORKTREE_NAME is not set. + * @returns {number} */ export function getWorktreePortOffset() { const name = process.env.WORKTREE_NAME; @@ -51,12 +70,18 @@ export function getWorktreePortOffset() { return ((Math.abs(hash) % 49) + 1) * 100; } -/** MongoDB port for the current worktree. */ +/** + * MongoDB port for the current worktree. + * @returns {number} + */ export function getMongoPort() { return 50000 + getWorktreePortOffset(); } -/** Azurite blob/queue/table ports for the current worktree. */ +/** + * Azurite blob/queue/table ports for the current worktree. + * @returns {AzuritePorts} + */ export function getAzuritePorts() { const offset = getWorktreePortOffset(); return { @@ -69,12 +94,14 @@ export function getAzuritePorts() { /** * Azurite connection string for worktree-specific ports. * Returns `UseDevelopmentStorage=true` for the default worktree (port 10000). + * @param {SettingsValues} [values] + * @returns {string} */ -export function getAzuriteConnectionString() { +export function getAzuriteConnectionString(values) { const ports = getAzuritePorts(); if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; - const accountName = getSetting('STORAGE_ACCOUNT_NAME'); - const accountKey = getSetting('STORAGE_ACCOUNT_KEY'); + const accountName = getSetting('STORAGE_ACCOUNT_NAME', values); + const accountKey = getSetting('STORAGE_ACCOUNT_KEY', values); if (!accountName || !accountKey) { throw new Error('[worktree-ports] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); } @@ -92,6 +119,7 @@ export function getAzuriteConnectionString() { * MongoDB connection string with the worktree-specific port patched in. * Reads COSMOSDB_CONNECTION_STRING from env or local.settings.json and replaces * the host:port segment. + * @returns {string} */ export function getMongoConnectionString() { const base = getSetting('COSMOSDB_CONNECTION_STRING'); diff --git a/sonar-project.properties b/sonar-project.properties index 141305444..0ede0a4b7 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -77,7 +77,7 @@ sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/* sonar.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/** # Coverage exclusions -sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx +sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,scripts/local-dev/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx # CPD (code duplication) exclusions sonar.cpd.exclusions=**/*.test.ts,**/*.generated.ts,**/*.generated.tsx,packages/cellix/archunit-tests/src/test-suites/**,packages/cellix/archunit-tests/src/fixtures/** From 6c415ea3221aa9e8175ab576ee270484de5fcb37 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 26 May 2026 15:56:09 -0400 Subject: [PATCH 05/40] small fixes for build file issue --- .../src/shared/support/servers/app-paths.ts | 10 ++- .../src/shared/support/servers/port-ready.ts | 29 +++++++ .../support/servers/resolve-portless.ts | 6 +- .../support/servers/test-azurite-server.ts | 17 +--- .../shared/support/servers/worktree-ports.ts | 22 +++++ .../src/settings/portless-settings.ts | 82 ++++++++++++++++--- 6 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts index b0ebf360f..d12c15632 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts @@ -1,9 +1,11 @@ +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -const workspaceRootUrl = new URL('../../../../../../../', import.meta.url); +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../../../..'); export const appPaths = { - apiDir: fileURLToPath(new URL('apps/api/', workspaceRootUrl)), - oauth2MockDir: fileURLToPath(new URL('apps/server-oauth2-mock/', workspaceRootUrl)), - uiCommunityDir: fileURLToPath(new URL('apps/ui-community/', workspaceRootUrl)), + apiDir: resolve(workspaceRoot, 'apps/api'), + oauth2MockDir: resolve(workspaceRoot, 'apps/server-oauth2-mock'), + uiCommunityDir: resolve(workspaceRoot, 'apps/ui-community'), } as const; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts new file mode 100644 index 000000000..548862d56 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts @@ -0,0 +1,29 @@ +import net from 'node:net'; + +interface WaitForPortOptions { + timeoutMs?: number; + intervalMs?: number; +} + +export function isPortListening(port: number): Promise { + return new Promise((resolve) => { + const socket = net.createConnection({ port, host: '127.0.0.1' }); + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + }); +} + +export async function waitForPort(port: number, { timeoutMs = 30_000, intervalMs = 250 }: WaitForPortOptions = {}): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isPortListening(port)) return true; + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + return false; +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts index 8c5cb9333..42c7f7827 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts @@ -1,14 +1,14 @@ import { existsSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -const currentDir = fileURLToPath(new URL('.', import.meta.url)); +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../../../..'); let resolvedPath: string | undefined; export function getPortlessPath(): string { if (!resolvedPath) { - const workspaceRoot = resolve(currentDir, '../../../../../../..'); const localBin = resolve(workspaceRoot, 'node_modules/.bin/portless'); if (existsSync(localBin)) { resolvedPath = localBin; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index 47558526a..2c8e92b0b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -4,21 +4,8 @@ import type { TestServer } from '@ocom-verification/verification-shared/servers' import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { appPaths } from './app-paths.ts'; import { spawnEnv } from './child-process-env.ts'; - -interface PortReadyModule { - isPortListening(port: number): Promise; - waitForPort(port: number, options?: { timeoutMs?: number; intervalMs?: number }): Promise; -} - -interface WorktreePortsModule { - getAzuritePorts(): { blob: number; queue: number; table: number }; -} - -const portReadyModuleUrl = new URL('../../../../../../../scripts/local-dev/port-ready.mjs', import.meta.url).href; -const worktreePortsModuleUrl = new URL('../../../../../../../scripts/local-dev/worktree-ports.mjs', import.meta.url).href; - -const { isPortListening, waitForPort } = (await import(portReadyModuleUrl)) as PortReadyModule; -const { getAzuritePorts } = (await import(worktreePortsModuleUrl)) as WorktreePortsModule; +import { isPortListening, waitForPort } from './port-ready.ts'; +import { getAzuritePorts } from './worktree-ports.ts'; /** * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts new file mode 100644 index 000000000..29f367936 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts @@ -0,0 +1,22 @@ +interface AzuritePorts { + blob: number; + queue: number; + table: number; +} + +export function getWorktreePortOffset(): number { + const name = process.env['WORKTREE_NAME']; + if (!name) return 0; + let hash = 0; + for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; + return ((Math.abs(hash) % 49) + 1) * 100; +} + +export function getAzuritePorts(): AzuritePorts { + const offset = getWorktreePortOffset(); + return { + blob: 10000 + offset, + queue: 10001 + offset, + table: 10002 + offset, + }; +} diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index a77b96e19..892c540d8 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,9 +1,7 @@ -/** - * Runtime bridge to the canonical portless hostname helpers used by both the - * dev:worktree scripts and the E2E test harness. Keeping the .mjs file as - * the single source of truth means there is exactly one place that derives - * hostnames from .env and applies the WORKTREE_NAME suffix. - */ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + interface PortlessHostnames { uiCommunity: string; uiStaff: string; @@ -12,14 +10,74 @@ interface PortlessHostnames { docs: string; } -interface PortlessHostnamesModule { - PORTLESS_PORT: number; - getHostnames(): PortlessHostnames; - buildPortlessUrl(hostname: string, path?: string): string; +type DotEnvValues = Record; + +const PORTLESS_PORT = 1355; +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../..'); +const uiCommunityEnvPath = resolve(workspaceRoot, 'apps/ui-community/.env'); +const uiStaffEnvPath = resolve(workspaceRoot, 'apps/ui-staff/.env'); + +function buildPortlessUrl(hostname: string, path = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${path}`; +} + +function getHostnames(): PortlessHostnames { + const uiCommunityEnv = readDotEnv(uiCommunityEnvPath); + const uiStaffEnv = readDotEnv(uiStaffEnvPath); + + const hostnames = { + uiCommunity: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_BASE_URL', uiCommunityEnvPath), + uiStaff: requireHostname(uiStaffEnv, 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI', uiStaffEnvPath), + api: requireHostname(uiCommunityEnv, 'VITE_COMMON_API_ENDPOINT', uiCommunityEnvPath), + mockAuth: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiCommunityEnvPath), + }; + + return applyWorktreeSuffixes(hostnames, process.env['WORKTREE_NAME'] ?? ''); } -const portlessHostnamesModuleUrl = new URL('../../../../../scripts/local-dev/portless-hostnames.mjs', import.meta.url).href; +function readDotEnv(filePath: string): DotEnvValues { + if (!existsSync(filePath)) return {}; + const result: DotEnvValues = {}; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + return result; +} -const { buildPortlessUrl, getHostnames, PORTLESS_PORT } = (await import(portlessHostnamesModuleUrl)) as PortlessHostnamesModule; +function hostnameFrom(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +function requireHostname(values: DotEnvValues, key: string, filePath: string): string { + const hostname = hostnameFrom(values[key] ?? ''); + if (!hostname) { + throw new Error(`portless-settings: could not derive hostname from ${key} in ${filePath}`); + } + return hostname; +} + +function applyWorktreeSuffixes(hostnames: Omit, worktreeName: string): PortlessHostnames { + return { + uiCommunity: applyWorktreeSuffix(hostnames.uiCommunity, worktreeName), + uiStaff: applyWorktreeSuffix(hostnames.uiStaff, worktreeName), + api: applyWorktreeSuffix(hostnames.api, worktreeName), + mockAuth: applyWorktreeSuffix(hostnames.mockAuth, worktreeName), + docs: applyWorktreeSuffix(`docs.${hostnames.uiCommunity}`, worktreeName), + }; +} + +function applyWorktreeSuffix(hostname: string, worktreeName: string): string { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} export { buildPortlessUrl, getHostnames, PORTLESS_PORT }; From 395bfcd4a6441536226dd1a18a2a5d011d4aaf31 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 27 May 2026 15:01:04 -0400 Subject: [PATCH 06/40] small improvements based on my comments, simplified overcomplicated logic --- apps/api/start-azurite.mjs | 17 +--- .../start-mongo.mjs | 6 -- apps/ui-community/start-dev.mjs | 2 +- apps/ui-staff/start-dev.mjs | 2 +- package.json | 1 + .../archunit-tests/src/validate-env-names.cjs | 11 --- .../support/servers/child-process-env.ts | 14 +--- .../src/shared/support/servers/port-ready.ts | 29 ------- .../shared/support/servers/portless-server.ts | 77 ++----------------- .../shared/support/servers/test-api-server.ts | 22 ------ .../support/servers/test-azurite-server.ts | 60 +++++++++++---- .../servers/test-community-vite-server.ts | 5 +- .../support/servers/test-environment.ts | 21 +++++ .../support/servers/test-oauth2-server.ts | 5 +- .../shared/support/shared-infrastructure.ts | 17 ---- .../ocom-verification/e2e-tests/turbo.json | 4 - .../src/settings/portless-settings.ts | 43 +++-------- scripts/local-dev/dev-process-exit.mjs | 17 +--- scripts/local-dev/port-ready.mjs | 45 ----------- scripts/local-dev/portless-hostnames.mjs | 45 ++++------- 20 files changed, 106 insertions(+), 337 deletions(-) delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts delete mode 100644 scripts/local-dev/port-ready.mjs diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index cc253db13..eb2f6146b 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -1,17 +1,11 @@ import { spawn } from 'node:child_process'; import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { isPortListening, waitForPort } from '../../scripts/local-dev/port-ready.mjs'; import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs'; const ports = getAzuritePorts(); const worktreeName = process.env.WORKTREE_NAME ?? ''; const storageSuffix = worktreeName ? `-${worktreeName}` : ''; -if (await isPortListening(ports.blob)) { - console.log(`[azurite] ready (blob port ${ports.blob})`); - process.exit(0); -} - const blobDir = `../../__blobstorage__${storageSuffix}`; const queueDir = `../../__queuestorage__${storageSuffix}`; const tableDir = `../../__tablestorage__${storageSuffix}`; @@ -22,6 +16,8 @@ const procs = [ spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), ]; +console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`); + let exited = 0; for (const proc of procs) { proc.on('exit', (code, signal) => { @@ -34,15 +30,6 @@ for (const proc of procs) { process.exit(code ?? 1); }); } - -if (await waitForPort(ports.blob, { timeoutMs: 30_000 })) { - console.log(`[azurite] ready (blob port ${ports.blob})`); -} else { - console.error(`[azurite] blob port ${ports.blob} did not become ready within 30000ms`); - for (const p of procs) p.kill(); - process.exit(1); -} - process.on('SIGINT', () => { for (const p of procs) p.kill('SIGINT'); }); diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 8d15a147e..30aa60095 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,13 +1,7 @@ -import { isPortListening } from '../../scripts/local-dev/port-ready.mjs'; import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; const MONGO_PORT = getMongoPort(); -if (await isPortListening(MONGO_PORT)) { - console.log(`[mongo-mock] already running on port ${MONGO_PORT}, skipping`); - process.exit(0); -} - // Not running — start it via tsx with the worktree-scoped port const { default: { spawn }, diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 62c79117f..7767c64f9 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -14,7 +14,7 @@ if (process.env.WORKTREE_NAME) { } const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; -const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +const viteMode = process.env.E2E_VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); } diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 0ef5c7673..8232af298 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -12,7 +12,7 @@ if (process.env.WORKTREE_NAME) { } const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; -const viteMode = process.env.VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); +const viteMode = process.env.E2E_VITE_MODE ?? (process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); } diff --git a/package.json b/package.json index 46e1f3dd3..e1fabbbbd 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", "test:e2e": "turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:e2e:worktree": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", + "test:e2e:workspace": "WORKTREE_NAME=$(basename $PWD) turbo run test:e2e --filter=@ocom-verification/e2e-tests", "test:acceptance": "turbo run test:acceptance --filter=@ocom-verification/acceptance-api --filter=@ocom-verification/acceptance-ui", "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", "test:integration": "turbo run test:integration", diff --git a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs index 2097f480c..827e78164 100644 --- a/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs +++ b/packages/ocom-verification/archunit-tests/src/validate-env-names.cjs @@ -8,7 +8,6 @@ const SKIP_DIRS = new Set(['node_modules', '.turbo', 'build-artifacts', 'dist', // Single source of truth for known portal identifiers (ADR-0031). // Keep this list in sync with apps/docs/docs/portals/PORTAL_REGISTRY.md. const CANONICAL_PORTALS = ['UI_COMMUNITY', 'UI_STAFF']; -const VITE_RESERVED_VARS = new Set(['VITE_MODE']); function walkDir(dir, fileList = []) { const files = fs.readdirSync(dir, { withFileTypes: true }); @@ -320,16 +319,6 @@ function validateEnvNames(options = {}) { for (const m of matches) { const variable = m.match; const relPath = `${path.relative(rootDir, filePath)}:${getLineNumber(newlineOffsets, m.index)}`; - if (VITE_RESERVED_VARS.has(variable)) { - results.push({ - variable, - status: 'compliant', - portal: 'BUILD', - ownerGroup: 'ocm-platform', - location: relPath, - }); - continue; - } if (!variable.startsWith('VITE_APP_') && !variable.startsWith('VITE_COMMON_')) { results.push({ variable, diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts index 51e02949c..1ce570cd1 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts @@ -1,14 +1,4 @@ -/** - * Returns a shallow copy of `process.env` with `NODE_OPTIONS` removed. - * - * Cucumber runs with `NODE_OPTIONS='--import tsx/esm'` so that TypeScript - * source is executed directly. Child processes spawned by the test harness - * (Azurite, portless, func) are plain JavaScript and do not have `tsx` on - * their resolution path, so inheriting `NODE_OPTIONS` causes an immediate - * crash. Callers can spread additional overrides on top of the result. - */ export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { - const env = { ...process.env, ...overrides }; - delete env['NODE_OPTIONS']; - return env; + const { NODE_OPTIONS: _ignored, ...baseEnv } = process.env; + return { ...baseEnv, ...overrides }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts deleted file mode 100644 index 548862d56..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/port-ready.ts +++ /dev/null @@ -1,29 +0,0 @@ -import net from 'node:net'; - -interface WaitForPortOptions { - timeoutMs?: number; - intervalMs?: number; -} - -export function isPortListening(port: number): Promise { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - -export async function waitForPort(port: number, { timeoutMs = 30_000, intervalMs = 250 }: WaitForPortOptions = {}): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await isPortListening(port)) return true; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return false; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts index a4dcfaa4b..c5e8aff5f 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts @@ -16,9 +16,7 @@ import { getPortlessPath } from './resolve-portless.ts'; export abstract class PortlessServer implements TestServer { private process: ChildProcess | null = null; private startedByUs = false; - private readonly useDetachedProcessGroup = process.platform !== 'win32'; - protected abstract get probeUrl(): string; protected abstract get readyMarker(): string; protected abstract get serverName(): string; protected abstract get spawnArgs(): string[]; @@ -28,51 +26,25 @@ export abstract class PortlessServer implements TestServer { return getPortlessPath(); } - protected get probeRequestInit(): RequestInit { - return {}; - } - protected get extraEnv(): Record { return {}; } - protected isProbeHealthy(response: Response): boolean | Promise { - return response.ok; - } - protected get startupTimeoutMs(): number { return getTimeout('serverStartup'); } abstract getUrl(): string; - /** - * Check if server is already running (via health probe). - */ - async isAlreadyRunning(): Promise { - try { - const controller = new AbortController(); - const probeTimeout = getTimeout('healthProbe'); - const timeout = setTimeout(() => controller.abort(), probeTimeout); - const res = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); - clearTimeout(timeout); - return await this.isProbeHealthy(res); - } catch { - return false; - } - } - /** * Start the server subprocess and wait for it to be ready. */ async start(): Promise { if (this.process || this.startedByUs) return; - if (await this.isAlreadyRunning()) return; this.process = spawn(this.executable, this.spawnArgs, { cwd: this.cwd, env: spawnEnv(this.extraEnv), - detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; @@ -90,15 +62,12 @@ export abstract class PortlessServer implements TestServer { this.process = null; this.startedByUs = false; - // SIGINT lets portless run its cleanup branch — deregister the hostname from - // ~/.portless/routes.json before exiting. Fall back to SIGKILL after the - // shutdown timeout for anything that ignores SIGINT. - this.killProcess(proc, 'SIGINT'); + proc.kill('SIGINT'); const shutdownTimeout = getTimeout('serverShutdown'); await new Promise((resolve) => { const timeout = setTimeout(() => { - this.killProcess(proc, 'SIGKILL'); + proc.kill('SIGKILL'); resolve(); }, shutdownTimeout); @@ -129,26 +98,12 @@ export abstract class PortlessServer implements TestServer { let stderrOutput = ''; let ready = false; - const resolveWhenReachable = () => { - if (ready) return; - ready = true; - - this.waitForProbeReady() - .then(() => { - clearTimeout(timeout); - resolve(); - }) - .catch((error: unknown) => { - clearTimeout(timeout); - reject(error); - }); - }; - - // stdout listener detects the readyMarker then waits for the probe to respond proc.stdout?.on('data', (data: Buffer) => { const text = data.toString(); - if (text.includes(this.readyMarker)) { - resolveWhenReachable(); + if (!ready && text.includes(this.readyMarker)) { + ready = true; + clearTimeout(timeout); + resolve(); } }); @@ -163,6 +118,7 @@ export abstract class PortlessServer implements TestServer { }); proc.on('exit', (code, signal) => { + if (ready) return; clearTimeout(timeout); this.process = null; this.startedByUs = false; @@ -170,23 +126,4 @@ export abstract class PortlessServer implements TestServer { }); }); } - - private async waitForProbeReady(): Promise { - const probeInterval = getTimeout('healthProbeInterval'); - while (!(await this.isAlreadyRunning())) { - await new Promise((resolve) => setTimeout(resolve, probeInterval)); - } - } - - private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { - if (this.useDetachedProcessGroup && proc.pid) { - try { - process.kill(-proc.pid, signal); - return; - } catch { - /* Fall back to killing the direct child below. */ - } - } - proc.kill(signal); - } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts index 3a45a7176..601df0683 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts @@ -10,28 +10,6 @@ const hostnames = getHostnames(); * from the committed e2e local settings plus runtime-only test values. */ export class TestApiServer extends PortlessServer { - protected get probeUrl() { - return buildUrl(hostnames.api, '/api/graphql'); - } - - protected override get probeRequestInit(): RequestInit { - return { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: '{ __typename }' }), - }; - } - - protected override async isProbeHealthy(response: Response): Promise { - if (!response.ok) return false; - try { - const data = (await response.json()) as { data?: { __typename?: string } }; - return data?.data?.__typename === 'Query'; - } catch { - return false; - } - } - protected get readyMarker() { return 'Functions:'; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts index 2c8e92b0b..9f3c8f28b 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts @@ -4,13 +4,12 @@ import type { TestServer } from '@ocom-verification/verification-shared/servers' import { getTimeout } from '@ocom-verification/verification-shared/settings'; import { appPaths } from './app-paths.ts'; import { spawnEnv } from './child-process-env.ts'; -import { isPortListening, waitForPort } from './port-ready.ts'; import { getAzuritePorts } from './worktree-ports.ts'; /** - * Starts Azurite via apps/api/start-azurite.mjs. The script itself short-circuits - * if the blob port is already listening, so concurrent worktrees and re-runs are - * safe. We track the spawned process only when we started it ourselves. + * Starts Azurite via apps/api/start-azurite.mjs. + * If ports are already bound (EADDRINUSE), we treat that as an existing + * reusable instance for this worktree. */ export class TestAzuriteServer implements TestServer { private process: ChildProcess | null = null; @@ -23,19 +22,19 @@ export class TestAzuriteServer implements TestServer { async start(): Promise { if (this.process || this.startedByUs) return; - if (await isPortListening(this.blobPort)) return; const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); + const { PATH: pathValue = '' } = process.env; this.process = spawn('node', ['start-azurite.mjs'], { cwd: appPaths.apiDir, - env: spawnEnv({ PATH: `${binDir}:${process.env['PATH'] ?? ''}` }), + env: spawnEnv({ PATH: `${binDir}:${pathValue}` }), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); this.startedByUs = true; - await this.waitForReady(); + await this.waitForStartedMarker(); } async stop(): Promise { @@ -68,14 +67,47 @@ export class TestAzuriteServer implements TestServer { return `http://127.0.0.1:${this.blobPort}`; } - private async waitForReady(): Promise { - const ready = await waitForPort(this.blobPort, { - timeoutMs: getTimeout('serverStartup'), - intervalMs: getTimeout('healthProbeInterval'), + private waitForStartedMarker(): Promise { + return new Promise((resolve, reject) => { + const proc = this.process; + if (!proc) { + reject(new Error('TestAzuriteServer process not started')); + return; + } + + const timeout = setTimeout(() => { + reject(new Error(`TestAzuriteServer did not emit start marker within ${getTimeout('serverStartup')}ms`)); + }, getTimeout('serverStartup')); + + let stderrOutput = ''; + + proc.stdout?.on('data', (data: Buffer) => { + if (data.toString().includes('[azurite] started')) { + clearTimeout(timeout); + resolve(); + } + }); + + proc.stderr?.on('data', (data: Buffer) => { + stderrOutput += data.toString(); + }); + + proc.on('error', (error: Error) => { + clearTimeout(timeout); + reject(new Error(`TestAzuriteServer failed to start: ${error.message}`)); + }); + + proc.on('exit', (code, signal) => { + clearTimeout(timeout); + if (stderrOutput.includes('EADDRINUSE')) { + this.process = null; + this.startedByUs = false; + resolve(); + return; + } + reject(new Error(`TestAzuriteServer exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); + }); }); - if (!ready) { - throw new Error(`TestAzuriteServer: blob port ${this.blobPort} did not start within timeout`); - } } } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts index 2251dba3c..c6a26237a 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts @@ -12,9 +12,6 @@ const hostnames = getHostnames(); * are needed here beyond suppressing browser auto-launch. */ export class TestCommunityViteServer extends PortlessServer { - protected get probeUrl() { - return buildUrl(hostnames.uiCommunity); - } protected get readyMarker() { return 'ready in'; } @@ -31,7 +28,7 @@ export class TestCommunityViteServer extends PortlessServer { return { BROWSER: 'none', NODE_ENV: 'development', - VITE_MODE: 'e2e', + E2E_VITE_MODE: 'e2e', }; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts index d9fa97a66..067b085c9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts @@ -1,10 +1,15 @@ import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; let mongoConnectionString: string | undefined; +loadE2EEnvDefaults(); + /** Module-level hostnames derived from .env files (matches dev:worktree pattern). */ const hostnames = getHostnames(); @@ -47,3 +52,19 @@ export function cleanupTestEnvironment(): void { proxyInitialized = false; mongoConnectionString = undefined; } + +function loadE2EEnvDefaults(): void { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const workspaceRoot = resolve(currentDir, '../../../../../../..'); + for (const filePath of [resolve(workspaceRoot, 'apps/ui-community/.env.e2e'), resolve(workspaceRoot, 'apps/ui-staff/.env.e2e')]) { + if (!existsSync(filePath)) continue; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx === -1) continue; + const key = trimmed.slice(0, idx); + process.env[key] ??= trimmed.slice(idx + 1); + } + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts index d21f0e090..f0c1b65c7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts @@ -1,6 +1,6 @@ import { appPaths } from './app-paths.ts'; import { PortlessServer } from './portless-server.ts'; -import { getHostnames, mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; +import { getHostnames, mockOidcIssuer } from './test-environment.ts'; const hostnames = getHostnames(); @@ -11,9 +11,6 @@ const hostnames = getHostnames(); * by programmatic token generation — this tests the real OIDC redirect flow. */ export class TestOAuth2Server extends PortlessServer { - protected get probeUrl() { - return mockOidcEndpoint; - } protected get readyMarker() { return 'Registered OIDC config'; } diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts index 7a793aa96..e42eb0d26 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts @@ -14,22 +14,6 @@ let browserBaseUrl: string | undefined; let authenticatedBrowserContext: BrowserContext | undefined; let browseTheWeb: BrowseTheWeb | undefined; -let shutdownHandlersRegistered = false; - -function registerShutdownHandlers(): void { - if (shutdownHandlersRegistered) return; - shutdownHandlersRegistered = true; - - const shutdown = (signal: string) => { - void stopAll().finally(() => { - process.exit(signal === 'SIGINT' ? 130 : 143); - }); - }; - - process.once('SIGINT', () => shutdown('SIGINT')); - process.once('SIGTERM', () => shutdown('SIGTERM')); -} - export interface InfrastructureState { apiUrl: string | undefined; browseTheWeb: BrowseTheWeb | undefined; @@ -77,7 +61,6 @@ export async function stopAll(): Promise { } export async function ensureE2EServers(): Promise { - registerShutdownHandlers(); initTestEnvironment(); // Phase 1: Start MongoDB, Azurite, and OAuth2 in parallel (no interdependency) diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index a5adb33a1..f677b6e40 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -8,9 +8,7 @@ "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", - "$TURBO_ROOT$/apps/ui-community/.env", "$TURBO_ROOT$/apps/ui-community/.env.e2e", - "$TURBO_ROOT$/apps/ui-staff/.env", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", "$TURBO_ROOT$/scripts/local-dev/**", "cucumber.js", @@ -25,9 +23,7 @@ "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", - "$TURBO_ROOT$/apps/ui-community/.env", "$TURBO_ROOT$/apps/ui-community/.env.e2e", - "$TURBO_ROOT$/apps/ui-staff/.env", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", "$TURBO_ROOT$/scripts/local-dev/**", "cucumber.js", diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts index 892c540d8..9415b0a64 100644 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts @@ -1,7 +1,3 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - interface PortlessHostnames { uiCommunity: string; uiStaff: string; @@ -10,43 +6,22 @@ interface PortlessHostnames { docs: string; } -type DotEnvValues = Record; - const PORTLESS_PORT = 1355; -const currentDir = dirname(fileURLToPath(import.meta.url)); -const workspaceRoot = resolve(currentDir, '../../../../..'); -const uiCommunityEnvPath = resolve(workspaceRoot, 'apps/ui-community/.env'); -const uiStaffEnvPath = resolve(workspaceRoot, 'apps/ui-staff/.env'); function buildPortlessUrl(hostname: string, path = ''): string { return `https://${hostname}:${PORTLESS_PORT}${path}`; } function getHostnames(): PortlessHostnames { - const uiCommunityEnv = readDotEnv(uiCommunityEnvPath); - const uiStaffEnv = readDotEnv(uiStaffEnvPath); - const hostnames = { - uiCommunity: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_BASE_URL', uiCommunityEnvPath), - uiStaff: requireHostname(uiStaffEnv, 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI', uiStaffEnvPath), - api: requireHostname(uiCommunityEnv, 'VITE_COMMON_API_ENDPOINT', uiCommunityEnvPath), - mockAuth: requireHostname(uiCommunityEnv, 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiCommunityEnvPath), + uiCommunity: requireHostname('VITE_APP_UI_COMMUNITY_BASE_URL'), + uiStaff: requireHostname('VITE_APP_UI_STAFF_AAD_REDIRECT_URI'), + api: requireHostname('VITE_COMMON_API_ENDPOINT'), + mockAuth: requireHostname('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'), }; + const { WORKTREE_NAME: worktreeName = '' } = process.env; - return applyWorktreeSuffixes(hostnames, process.env['WORKTREE_NAME'] ?? ''); -} - -function readDotEnv(filePath: string): DotEnvValues { - if (!existsSync(filePath)) return {}; - const result: DotEnvValues = {}; - for (const line of readFileSync(filePath, 'utf-8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eqIdx = trimmed.indexOf('='); - if (eqIdx === -1) continue; - result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); - } - return result; + return applyWorktreeSuffixes(hostnames, worktreeName); } function hostnameFrom(url: string): string | null { @@ -57,10 +32,10 @@ function hostnameFrom(url: string): string | null { } } -function requireHostname(values: DotEnvValues, key: string, filePath: string): string { - const hostname = hostnameFrom(values[key] ?? ''); +function requireHostname(key: string): string { + const hostname = hostnameFrom(process.env[key] ?? ''); if (!hostname) { - throw new Error(`portless-settings: could not derive hostname from ${key} in ${filePath}`); + throw new Error(`portless-settings: required env var ${key} is missing or invalid`); } return hostname; } diff --git a/scripts/local-dev/dev-process-exit.mjs b/scripts/local-dev/dev-process-exit.mjs index d762c4d5e..ed0232915 100644 --- a/scripts/local-dev/dev-process-exit.mjs +++ b/scripts/local-dev/dev-process-exit.mjs @@ -1,26 +1,11 @@ /** @typedef {import('node:child_process').ChildProcess} ChildProcess */ -const INTERRUPT_SIGNALS = new Set(['SIGINT', 'SIGTERM', 'SIGQUIT']); -const INTERRUPT_EXIT_CODES = new Set([130, 143]); - -/** - * @param {NodeJS.Signals | null | undefined} signal - * @returns {boolean} - */ -export const isInterruptSignal = (signal) => Boolean(signal && INTERRUPT_SIGNALS.has(signal)); - -/** - * @param {number | null | undefined} code - * @returns {boolean} - */ -export const isInterruptExitCode = (code) => Number.isInteger(code) && INTERRUPT_EXIT_CODES.has(/** @type {number} */ (code)); - /** * @param {NodeJS.Signals | null | undefined} signal * @param {number | null | undefined} code * @returns {boolean} */ -export const isGracefulInterruptExit = (signal, code) => isInterruptSignal(signal) || isInterruptExitCode(code); +export const isGracefulInterruptExit = (signal, code) => signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT' || code === 130 || code === 143; /** * Wires a spawned dev child process to forward its exit status to the parent, diff --git a/scripts/local-dev/port-ready.mjs b/scripts/local-dev/port-ready.mjs deleted file mode 100644 index a24930f35..000000000 --- a/scripts/local-dev/port-ready.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import net from 'node:net'; - -/** - * @typedef {object} WaitForPortOptions - * @property {number} [timeoutMs] - * @property {number} [intervalMs] - */ - -/** - * Resolves true if a TCP listener is accepting connections on the given port - * on 127.0.0.1. Used by the dev launchers to short-circuit when a service is - * already running (Azurite, Mongo) and by the e2e harness to wait for ports - * to come up. - * @param {number} port - * @returns {Promise} - */ -export function isPortListening(port) { - return new Promise((resolve) => { - const socket = net.createConnection({ port, host: '127.0.0.1' }); - socket.once('connect', () => { - socket.destroy(); - resolve(true); - }); - socket.once('error', () => { - socket.destroy(); - resolve(false); - }); - }); -} - -/** - * Polls `isPortListening(port)` until it resolves true or the timeout elapses. - * Resolves true on success, false on timeout — callers decide how to react. - * @param {number} port - * @param {WaitForPortOptions} [options] - * @returns {Promise} - */ -export async function waitForPort(port, { timeoutMs = 30_000, intervalMs = 250 } = {}) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (await isPortListening(port)) return true; - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return false; -} diff --git a/scripts/local-dev/portless-hostnames.mjs b/scripts/local-dev/portless-hostnames.mjs index b2e9deb01..976c82e92 100644 --- a/scripts/local-dev/portless-hostnames.mjs +++ b/scripts/local-dev/portless-hostnames.mjs @@ -1,20 +1,3 @@ -/** - * Shared portless hostname computation for git worktree isolation. - * - * Hostnames are derived from the tracked .env files, so this module contains - * no hardcoded service names. When `WORKTREE_NAME` is set the worktree suffix - * is spliced in before `.localhost`, giving each worktree its own subdomain - * on the shared proxy port. - * - * Default (no worktree): - * ownercommunity.localhost (read from VITE_APP_UI_COMMUNITY_BASE_URL) - * data-access.ownercommunity.localhost (read from VITE_COMMON_API_ENDPOINT) - * - * With WORKTREE_NAME=feature-a: - * ownercommunity.feature-a.localhost - * data-access.ownercommunity.feature-a.localhost - */ - import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -64,42 +47,40 @@ function hostnameFrom(url) { } /** - * Splice `.` in before `.localhost` in an existing hostname. - * @param {string} hostname - * @param {string} worktreeName - * @returns {string} + * @param {string} key + * @param {DotEnvValues} values + * @returns {string | null} */ +function hostnameFor(key, values) { + return hostnameFrom(process.env[key] ?? values[key] ?? ''); +} + function applyWorktreeSuffix(hostname, worktreeName) { if (!worktreeName) return hostname; return hostname.replace('.localhost', `.${worktreeName}.localhost`); } -/** - * Returns all service hostnames scoped to the current worktree (if any). - * Hostname shapes are read from the tracked .env files — no names are - * hardcoded in this module. - * @returns {PortlessHostnames} - */ export function getHostnames() { const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); const staffEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-staff/.env')); const wt = process.env.WORKTREE_NAME ?? ''; - const uiCommunity = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_BASE_URL'] ?? ''); - const api = hostnameFrom(uiEnv['VITE_COMMON_API_ENDPOINT'] ?? ''); - const mockAuth = hostnameFrom(uiEnv['VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'] ?? ''); - const uiStaff = hostnameFrom(staffEnv['VITE_APP_UI_STAFF_AAD_REDIRECT_URI'] ?? ''); + const uiCommunity = hostnameFor('VITE_APP_UI_COMMUNITY_BASE_URL', uiEnv); + const api = hostnameFor('VITE_COMMON_API_ENDPOINT', uiEnv); + const mockAuth = hostnameFor('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiEnv); + const uiStaff = hostnameFor('VITE_APP_UI_STAFF_AAD_REDIRECT_URI', staffEnv); if (!uiCommunity || !api || !mockAuth || !uiStaff) { throw new Error('portless-hostnames: could not derive all hostnames from .env files. ' + 'Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); } + const docs = `docs.${uiCommunity}`; return { uiCommunity: applyWorktreeSuffix(uiCommunity, wt), uiStaff: applyWorktreeSuffix(uiStaff, wt), api: applyWorktreeSuffix(api, wt), mockAuth: applyWorktreeSuffix(mockAuth, wt), - docs: applyWorktreeSuffix(`docs.${uiCommunity}`, wt), + docs: applyWorktreeSuffix(docs, wt), }; } From 15226024a8afe0f57020abe2cd6704de14003e00 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 13:47:27 -0400 Subject: [PATCH 07/40] retrigger build --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 56b87aea2..526b5e88f 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo ## Introduction + [Getting Started](https://developers.cellixjs.org/docs/intro): Our Docusaurus website will help you get started in running and contributing to CellixJS From 843f3d06746402e22b67a4861ae38811b2b0766f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 14:22:44 -0400 Subject: [PATCH 08/40] sourcery suggestions pt1 --- apps/server-mongodb-memory-mock/start-mongo.mjs | 11 ++++------- apps/ui-community/start-dev.mjs | 5 ++++- apps/ui-staff/start-dev.mjs | 5 ++++- scripts/local-dev/worktree-ports.mjs | 4 +++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs index 30aa60095..cead8110f 100644 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ b/apps/server-mongodb-memory-mock/start-mongo.mjs @@ -1,15 +1,12 @@ +import { spawn } from 'node:child_process'; +import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; const MONGO_PORT = getMongoPort(); -// Not running — start it via tsx with the worktree-scoped port -const { - default: { spawn }, -} = await import('node:child_process'); const child = spawn('tsx', ['src/index.ts'], { stdio: 'inherit', env: { ...process.env, PORT: String(MONGO_PORT) }, }); -child.on('exit', (code) => { - process.exit(code ?? 1); -}); + +forwardChildExit(child); diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index 02965f584..ff5f6dc34 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -13,7 +13,10 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; +if (process.env.PORT) { + viteArgs.push('--port', process.env.PORT); +} const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 776cabcea..23265c8c1 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -11,7 +11,10 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const viteArgs = ['--port', process.env.PORT, '--host', process.env.HOST ?? '127.0.0.1']; +const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; +if (process.env.PORT) { + viteArgs.push('--port', process.env.PORT); +} const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); if (viteMode) { viteArgs.push('--mode', viteMode); diff --git a/scripts/local-dev/worktree-ports.mjs b/scripts/local-dev/worktree-ports.mjs index 25d843cbf..de11700d6 100644 --- a/scripts/local-dev/worktree-ports.mjs +++ b/scripts/local-dev/worktree-ports.mjs @@ -124,5 +124,7 @@ export function getAzuriteConnectionString(values) { export function getMongoConnectionString() { const base = getSetting('COSMOSDB_CONNECTION_STRING'); if (!base) throw new Error('[worktree-ports] COSMOSDB_CONNECTION_STRING must be set'); - return base.replace(/127\.0\.0\.1:\d+/, `127.0.0.1:${getMongoPort()}`); + const url = new URL(base); + url.port = String(getMongoPort()); + return url.toString(); } From c184d921660274ca936caf22f3beb7e74caf4f80 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 15:02:12 -0400 Subject: [PATCH 09/40] sourcery suggestions reound 2 - reusability --- apps/api/start-azurite.mjs | 17 +++++++++++++---- apps/ui-community/start-dev.mjs | 14 ++------------ apps/ui-staff/start-dev.mjs | 14 ++------------ scripts/local-dev/vite-dev-args.mjs | 26 ++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 scripts/local-dev/vite-dev-args.mjs diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs index eb2f6146b..8cb110b31 100644 --- a/apps/api/start-azurite.mjs +++ b/apps/api/start-azurite.mjs @@ -10,11 +10,20 @@ const blobDir = `../../__blobstorage__${storageSuffix}`; const queueDir = `../../__queuestorage__${storageSuffix}`; const tableDir = `../../__tablestorage__${storageSuffix}`; -const procs = [ - spawn('azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir], { stdio: 'inherit' }), - spawn('azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir], { stdio: 'inherit' }), - spawn('azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir], { stdio: 'inherit' }), +const procSpecs = [ + ['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir]], + ['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir]], + ['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir]], ]; +const procs = procSpecs.map(([command, args]) => { + const proc = spawn(command, args, { stdio: 'inherit' }); + proc.on('error', (error) => { + console.error(`[azurite] failed to start ${command}: ${error.message}`); + for (const p of procs) p.kill(); + process.exit(1); + }); + return proc; +}); console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`); diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs index ff5f6dc34..233be3229 100644 --- a/apps/ui-community/start-dev.mjs +++ b/apps/ui-community/start-dev.mjs @@ -1,6 +1,7 @@ import { spawn } from 'node:child_process'; import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; +import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; const childEnv = { ...process.env }; @@ -13,14 +14,7 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); } -const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; -if (process.env.PORT) { - viteArgs.push('--port', process.env.PORT); -} -const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); -if (viteMode) { - viteArgs.push('--mode', viteMode); -} +const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); const child = spawn('vite', viteArgs, { stdio: 'inherit', @@ -28,7 +22,3 @@ const child = spawn('vite', viteArgs, { }); forwardChildExit(child); - -function isE2E() { - return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase()); -} diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs index 23265c8c1..c18f42dcf 100644 --- a/apps/ui-staff/start-dev.mjs +++ b/apps/ui-staff/start-dev.mjs @@ -1,6 +1,7 @@ import { spawn } from 'node:child_process'; import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; +import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; const childEnv = { ...process.env }; @@ -11,14 +12,7 @@ if (process.env.WORKTREE_NAME) { childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); } -const viteArgs = ['--host', process.env.HOST ?? '127.0.0.1']; -if (process.env.PORT) { - viteArgs.push('--port', process.env.PORT); -} -const viteMode = process.env.E2E_VITE_MODE ?? (isE2E() || process.env.TF_BUILD ? 'e2e' : undefined); -if (viteMode) { - viteArgs.push('--mode', viteMode); -} +const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); const child = spawn('vite', viteArgs, { stdio: 'inherit', @@ -26,7 +20,3 @@ const child = spawn('vite', viteArgs, { }); forwardChildExit(child); - -function isE2E() { - return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase()); -} diff --git a/scripts/local-dev/vite-dev-args.mjs b/scripts/local-dev/vite-dev-args.mjs new file mode 100644 index 000000000..dcc39bc77 --- /dev/null +++ b/scripts/local-dev/vite-dev-args.mjs @@ -0,0 +1,26 @@ +/** + * @param {NodeJS.ProcessEnv} [env] + * @returns {boolean} + */ +export function isE2E(env = process.env) { + return ['1', 'true', 'yes'].includes((env.E2E ?? '').toLowerCase()); +} + +/** + * @param {{ host?: string; port?: string; env?: NodeJS.ProcessEnv }} [options] + * @returns {string[]} + */ +export function buildViteArgs(options = {}) { + const { host = '127.0.0.1', port, env = process.env } = options; + const args = ['--host', host]; + if (port) { + args.push('--port', port); + } + + const viteMode = env.E2E_VITE_MODE ?? (isE2E(env) || env.TF_BUILD ? 'e2e' : undefined); + if (viteMode) { + args.push('--mode', viteMode); + } + + return args; +} From fabfb36ef0ccad7249847af165d51f50f3b6d8b6 Mon Sep 17 00:00:00 2001 From: jasonmorais <120504390+jasonmorais@users.noreply.github.com> Date: Thu, 28 May 2026 15:11:04 -0400 Subject: [PATCH 10/40] Update readme.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 526b5e88f..886223cc6 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo [Getting Started](https://developers.cellixjs.org/docs/intro): -Our Docusaurus website will help you get started in running and contributing to CellixJS +Our Docusaurus website will help you get started with running and contributing to CellixJS ## Project Status From 0b7647786ab2266a74c146af4815b17951c21776 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 28 May 2026 16:01:20 -0400 Subject: [PATCH 11/40] undo tsc to tsgo --- packages/cellix/archunit-tests/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cellix/archunit-tests/package.json b/packages/cellix/archunit-tests/package.json index 3c1e6ba9c..96dcca505 100644 --- a/packages/cellix/archunit-tests/package.json +++ b/packages/cellix/archunit-tests/package.json @@ -43,8 +43,8 @@ }, "scripts": { "prebuild": "biome lint", - "build": "tsc --build", - "watch": "tsc --watch", + "build": "tsgo --build", + "watch": "tsgo --watch", "test": "vitest run", "test:arch": "vitest run", "test:coverage": "pnpm run test", From 110f771e1b4c97998cd42593012703f016bf534a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 29 May 2026 15:49:13 -0400 Subject: [PATCH 12/40] fix serenity pattern - use task pattern instead of interaction --- .../authentication/abilities/header-types.ts | 5 + .../step-definitions/header-login.steps.ts | 11 +- .../tasks/click-header-sign-in.ts | 14 ++ .../tasks/click-header-sign-in.ts | 18 +- .../community/tasks/create-community.ts | 20 ++- .../authentication/abilities/header-types.ts | 7 + .../step-definitions/header-login.steps.ts | 49 +----- .../tasks/click-header-sign-in.ts | 46 +++++ .../community/tasks/create-community.ts | 166 +++++++++--------- .../src/shared/support/oauth2-login.ts | 47 ++--- .../verification-shared/package.json | 2 + .../verification-shared/src/serenity/index.ts | 1 + .../src/serenity/task-step.ts | 14 ++ pnpm-lock.yaml | 35 ++-- pnpm-workspace.yaml | 2 +- 15 files changed, 244 insertions(+), 193 deletions(-) create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts create mode 100644 packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts create mode 100644 packages/ocom-verification/verification-shared/src/serenity/index.ts create mode 100644 packages/ocom-verification/verification-shared/src/serenity/task-step.ts diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts new file mode 100644 index 000000000..c9b39119a --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts @@ -0,0 +1,5 @@ +export interface HeaderApiNotes { + identityProviderUnreachable: boolean; + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; +} diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts index 8ab66301e..d204f57f9 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,11 +1,7 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; - -interface HeaderApiNotes { - identityProviderUnreachable: boolean; - signinRedirectInvoked: boolean; - fallbackTriggered: boolean; -} +import type { HeaderApiNotes } from '../abilities/header-types.ts'; +import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; let lastActorName = 'Alex'; @@ -32,8 +28,7 @@ Given('the identity provider is unreachable', async () => { When('{word} chooses to sign in', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - const unreachable = await actor.answer(notes().get('identityProviderUnreachable')); - await actor.attemptsTo(notes().set('signinRedirectInvoked', !unreachable), notes().set('fallbackTriggered', unreachable)); + await actor.attemptsTo(ClickHeaderSignIn()); }); Then('{word} is taken to the sign-in flow', async (actorName: string) => { diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts new file mode 100644 index 000000000..b0e46f773 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -0,0 +1,14 @@ +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; +import type { HeaderApiNotes } from '../abilities/header-types.ts'; + +export const ClickHeaderSignIn = () => + Task.where( + '#actor chooses to sign in through the authentication API', + new TaskStep('#actor requests the sign-in redirect state', async (serenityActor) => { + const actor = serenityActor as Actor; + const unreachable = await actor.answer(notes().get('identityProviderUnreachable')); + + await actor.attemptsTo(notes().set('signinRedirectInvoked', !unreachable), notes().set('fallbackTriggered', unreachable)); + }) as Activity, + ); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts index 8b634c058..5b36010c4 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,6 +1,7 @@ import { HomePage, type UiHomePage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { Interaction } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, Task } from '@serenity-js/core'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -9,10 +10,13 @@ async function flushAsync(): Promise { } export const ClickHeaderSignIn = (container: HTMLElement) => - Interaction.where('#actor clicks the sign-in button on the home page', async () => { - const adapter = new JsdomPageAdapter(container); - const page: UiHomePage = new HomePage(adapter); + Task.where( + '#actor clicks the sign-in button on the home page', + new TaskStep('#actor clicks the sign-in button', async () => { + const adapter = new JsdomPageAdapter(container); + const page: UiHomePage = new HomePage(adapter); - await page.clickSignIn(); - await flushAsync(); - }); + await page.clickSignIn(); + await flushAsync(); + }) as Activity, + ); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index 2c2683b3e..320723c11 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -1,6 +1,7 @@ import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { Interaction } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, Task } from '@serenity-js/core'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -12,12 +13,15 @@ async function flushAsync(): Promise { } export const CreateCommunity = (container: HTMLElement, name: string) => - Interaction.where(`#actor fills community name "${name}" and submits`, async () => { - const adapter = new JsdomPageAdapter(container); - const page: UiCommunityPage = new CommunityPage(adapter); + Task.where( + `#actor fills community name "${name}" and submits`, + new TaskStep(`#actor submits community name "${name}"`, async () => { + const adapter = new JsdomPageAdapter(container); + const page: UiCommunityPage = new CommunityPage(adapter); - await page.fillName(name); - await page.clickCreate(); + await page.fillName(name); + await page.clickCreate(); - await flushAsync(); - }); + await flushAsync(); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts new file mode 100644 index 000000000..b9ad079eb --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts @@ -0,0 +1,7 @@ +export interface HeaderE2ENotes { + signinRedirectInvoked: boolean; + fallbackTriggered: boolean; + postLoginUrl: string; +} + +export type HeaderE2ESite = 'community' | 'staff'; diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index ee45019e4..998ec4dcd 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -3,18 +3,12 @@ import { actorCalled, notes } from '@serenity-js/core'; import type { BrowserContext, Page } from 'playwright'; import * as infra from '../../../shared/support/shared-infrastructure.ts'; import type { CellixE2EWorld } from '../../../world.ts'; - -interface HeaderE2ENotes { - signinRedirectInvoked: boolean; - fallbackTriggered: boolean; - postLoginUrl: string; -} - -type Site = 'community' | 'staff'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; +import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; interface HeaderE2EState { actorName: string; - site: Site; + site: HeaderE2ESite; identityProviderUnreachable: boolean; context?: BrowserContext; page?: Page; @@ -29,7 +23,7 @@ function getHeaderState(world: HeaderE2EWorld): HeaderE2EState { return world.__headerState; } -function setHeaderState(world: HeaderE2EWorld, actorName: string, site: Site): HeaderE2EState { +function setHeaderState(world: HeaderE2EWorld, actorName: string, site: HeaderE2ESite): HeaderE2EState { const state: HeaderE2EState = { actorName, site, identityProviderUnreachable: false }; world.__headerState = state; return state; @@ -63,15 +57,10 @@ Given('the identity provider is unreachable', function (this: HeaderE2EWorld) { getHeaderState(this).identityProviderUnreachable = true; }); -// Credentials from apps/ui-{portal}/mock-oidc.users.json -const portalCredentials: Record = { - community: { username: 'test@example.com', password: 'password' }, - staff: { username: 'staff@ownercommunity.onmicrosoft.com', password: 'password' }, -}; - When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorName: string) { const s = getHeaderState(this); s.actorName = actorName; + const actor = actorCalled(actorName); const { browser } = infra.getState(); if (!browser) throw new Error('Browser not launched'); @@ -93,33 +82,7 @@ When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorNam const page = await context.newPage(); s.page = page; - // Navigate to site root — the unauthenticated home page is visible - await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); - - // Click the sign-in button on the home page - const signInButton = page.getByRole('button', { name: /Log In|Sign In/i }); - await signInButton.click(); - - if (s.identityProviderUnreachable) { - // IdP is blocked — the app should handle the error gracefully. - // Wait for error handling to settle, then leave the page open for Then to inspect. - await page.waitForTimeout(2000); - } else { - // Wait for redirect to mock-auth login form - await page.waitForURL((url) => url.hostname.includes('mock-auth'), { timeout: 15_000 }); - - // Complete the login form with portal-specific credentials - const creds = portalCredentials[s.site]; - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', creds.username); - await page.fill('input[name="password"]', creds.password); - await page.click('button[type="submit"]'); - } - - // Wait for the redirect chain to settle back on the portal - await page.waitForURL((url) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'), { timeout: 30_000 }); - await page.waitForLoadState('networkidle'); - } + await actor.attemptsTo(ClickHeaderSignIn(page, s.site, s.identityProviderUnreachable)); }); Then('{word} is taken to the sign-in flow', async function (this: HeaderE2EWorld, actorName: string) { diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts new file mode 100644 index 000000000..bdf690de1 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -0,0 +1,46 @@ +import { type E2EHomePage, HomePage } from '@ocom-verification/verification-shared/pages'; +import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; +import type { Page } from 'playwright'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; + +const portalCredentials: Record = { + community: { username: 'test@example.com', password: 'password' }, + staff: { username: 'staff@ownercommunity.onmicrosoft.com', password: 'password' }, +}; + +const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'); + +export const ClickHeaderSignIn = (page: Page, site: HeaderE2ESite, identityProviderUnreachable: boolean) => + Task.where( + '#actor clicks the sign-in button on the home page', + new TaskStep('#actor clicks the sign-in button on the home page', async (serenityActor) => { + const actor = serenityActor as Actor; + + await page.goto('/', { waitUntil: 'networkidle', timeout: 30_000 }); + + const adapter = new PlaywrightPageAdapter(page); + const homePage: E2EHomePage = new HomePage(adapter); + await homePage.clickSignIn(); + + if (identityProviderUnreachable) { + await page.waitForTimeout(2_000); + await actor.attemptsTo(notes().set('fallbackTriggered', true), notes().set('signinRedirectInvoked', false), notes().set('postLoginUrl', page.url())); + return; + } + + await page.waitForURL((url) => url.hostname.includes('mock-auth'), { timeout: 15_000 }); + + const creds = portalCredentials[site]; + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', creds.username); + await page.fill('input[name="password"]', creds.password); + await page.click('button[type="submit"]'); + } + + await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); + await page.waitForLoadState('networkidle'); + await actor.attemptsTo(notes().set('signinRedirectInvoked', true), notes().set('fallbackTriggered', false), notes().set('postLoginUrl', page.url())); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts index f24fb78f4..1f67af90e 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts @@ -1,6 +1,7 @@ import { CommunityPage, type E2ECommunityPage } from '@ocom-verification/verification-shared/pages'; import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; -import { type Actor, Interaction, notes, the } from '@serenity-js/core'; +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { type Activity, type Actor, notes, Task, the } from '@serenity-js/core'; import type { Response } from 'playwright'; import { BrowseTheWeb } from '../../../shared/abilities/browse-the-web.ts'; import type { CommunityE2ENotes } from '../abilities/community-types.ts'; @@ -63,83 +64,86 @@ const graphqlErrors = (payload: { errors?: Array<{ message?: string }> } | null) * Creates a community through the browser UI. */ export const CreateCommunity = (name: string) => - Interaction.where(the`#actor creates community "${name}" via UI`, async (serenityActor) => { - const actor = serenityActor as unknown as Actor; - const { page } = BrowseTheWeb.withActor(actor); - await page.goto('/community/accounts/create-community', { - waitUntil: 'networkidle', - }); - - const adapter = new PlaywrightPageAdapter(page); - const communityPage: E2ECommunityPage = new CommunityPage(adapter); - - await communityPage.fillName(name); - - const createMutationResponse = page.waitForResponse(hasGraphqlOperation(createCommunityOperationName), { timeout: 15_000 }).catch(() => null); - const communityListResponse = page.waitForResponse(hasGraphqlOperation(communityListOperationName), { timeout: 15_000 }).catch(() => null); - const memberListResponse = page.waitForResponse(hasGraphqlOperation(memberListOperationName), { timeout: 15_000 }).catch(() => null); - - await communityPage.clickCreate(); - - await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 750 }).catch(() => undefined); - const validationError = await communityPage.firstValidationError.isVisible().catch(() => false); - if (validationError) { - const errorText = await communityPage.firstValidationError.textContent(); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); - return; - } - - const mutationResponse = await createMutationResponse; - if (!mutationResponse) { - await communityPage.errorToast.waitFor({ state: 'visible', timeout: 1_000 }).catch(() => undefined); - const hasErrorToast = await communityPage.errorToast.isVisible().catch(() => false); - const errorText = hasErrorToast ? await communityPage.errorToast.textContent() : null; - const message = errorText || `No ${createCommunityOperationName} GraphQL response was received`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const payload = selectGraphqlPayload((await mutationResponse.json().catch(() => null)) as CommunityCreateGraphqlPayload | CommunityCreateGraphqlPayload[] | null, (data) => Boolean(data?.communityCreate)); - const graphqlError = graphqlErrors(payload); - const mutationResult = payload?.data?.communityCreate; - const mutationError = mutationResult?.status?.errorMessage ?? graphqlError; - const createdName = mutationResult?.community?.name ?? null; - - if (!mutationResponse.ok || graphqlError || mutationResult?.status?.success !== true || (createdName !== null && createdName !== name)) { - const message = - mutationError || - (mutationResult?.status?.success !== true - ? `${createCommunityOperationName} did not report success: ${JSON.stringify(payload)}` - : createdName !== name - ? `Expected created community name "${name}" but GraphQL returned "${createdName ?? 'null'}"` - : `Community create GraphQL request failed with HTTP ${mutationResponse.status()}`); - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const listResponse = await communityListResponse; - const listPayload = listResponse - ? selectGraphqlPayload((await listResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.communitiesForCurrentEndUser !== undefined) - : null; - const listGraphqlError = graphqlErrors(listPayload); - const listContainsCreatedCommunity = listPayload?.data?.communitiesForCurrentEndUser?.some((community) => community.name === name) ?? false; - if (!listResponse?.ok() || listGraphqlError || !listContainsCreatedCommunity) { - const message = listGraphqlError || `Expected "${name}" in ${communityListOperationName} response after creation`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - const membersResponse = await memberListResponse; - const membersPayload = membersResponse - ? selectGraphqlPayload((await membersResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.membersForCurrentEndUser !== undefined) - : null; - const membersGraphqlError = graphqlErrors(membersPayload); - if (!membersResponse?.ok() || membersGraphqlError) { - const message = membersGraphqlError || `${memberListOperationName} did not complete successfully after creation`; - await actor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); - throw new Error(message); - } - - await page.getByRole('cell', { name, exact: true }).first().waitFor({ state: 'visible', timeout: 5_000 }); - await actor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); - }); + Task.where( + the`#actor creates community "${name}" via UI`, + new TaskStep(`#actor submits community "${name}" through the browser UI`, async (actor) => { + const serenityActor = actor as Actor; + const { page } = BrowseTheWeb.withActor(serenityActor); + await page.goto('/community/accounts/create-community', { + waitUntil: 'networkidle', + }); + + const adapter = new PlaywrightPageAdapter(page); + const communityPage: E2ECommunityPage = new CommunityPage(adapter); + + await communityPage.fillName(name); + + const createMutationResponse = page.waitForResponse(hasGraphqlOperation(createCommunityOperationName), { timeout: 15_000 }).catch(() => null); + const communityListResponse = page.waitForResponse(hasGraphqlOperation(communityListOperationName), { timeout: 15_000 }).catch(() => null); + const memberListResponse = page.waitForResponse(hasGraphqlOperation(memberListOperationName), { timeout: 15_000 }).catch(() => null); + + await communityPage.clickCreate(); + + await communityPage.firstValidationError.waitFor({ state: 'visible', timeout: 750 }).catch(() => undefined); + const validationError = await communityPage.firstValidationError.isVisible().catch(() => false); + if (validationError) { + const errorText = await communityPage.firstValidationError.textContent(); + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', errorText || 'Validation error')); + return; + } + + const mutationResponse = await createMutationResponse; + if (!mutationResponse) { + await communityPage.errorToast.waitFor({ state: 'visible', timeout: 1_000 }).catch(() => undefined); + const hasErrorToast = await communityPage.errorToast.isVisible().catch(() => false); + const errorText = hasErrorToast ? await communityPage.errorToast.textContent() : null; + const message = errorText || `No ${createCommunityOperationName} GraphQL response was received`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const payload = selectGraphqlPayload((await mutationResponse.json().catch(() => null)) as CommunityCreateGraphqlPayload | CommunityCreateGraphqlPayload[] | null, (data) => Boolean(data?.communityCreate)); + const graphqlError = graphqlErrors(payload); + const mutationResult = payload?.data?.communityCreate; + const mutationError = mutationResult?.status?.errorMessage ?? graphqlError; + const createdName = mutationResult?.community?.name ?? null; + + if (!mutationResponse.ok || graphqlError || mutationResult?.status?.success !== true || (createdName !== null && createdName !== name)) { + const message = + mutationError || + (mutationResult?.status?.success !== true + ? `${createCommunityOperationName} did not report success: ${JSON.stringify(payload)}` + : createdName !== name + ? `Expected created community name "${name}" but GraphQL returned "${createdName ?? 'null'}"` + : `Community create GraphQL request failed with HTTP ${mutationResponse.status()}`); + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const listResponse = await communityListResponse; + const listPayload = listResponse + ? selectGraphqlPayload((await listResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.communitiesForCurrentEndUser !== undefined) + : null; + const listGraphqlError = graphqlErrors(listPayload); + const listContainsCreatedCommunity = listPayload?.data?.communitiesForCurrentEndUser?.some((community) => community.name === name) ?? false; + if (!listResponse?.ok() || listGraphqlError || !listContainsCreatedCommunity) { + const message = listGraphqlError || `Expected "${name}" in ${communityListOperationName} response after creation`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + const membersResponse = await memberListResponse; + const membersPayload = membersResponse + ? selectGraphqlPayload((await membersResponse.json().catch(() => null)) as CommunityListGraphqlPayload | CommunityListGraphqlPayload[] | null, (data) => data?.membersForCurrentEndUser !== undefined) + : null; + const membersGraphqlError = graphqlErrors(membersPayload); + if (!membersResponse?.ok() || membersGraphqlError) { + const message = membersGraphqlError || `${memberListOperationName} did not complete successfully after creation`; + await serenityActor.attemptsTo(notes().set('communityCreated', false), notes().set('errorMessage', message)); + throw new Error(message); + } + + await page.getByRole('cell', { name, exact: true }).first().waitFor({ state: 'visible', timeout: 5_000 }); + await serenityActor.attemptsTo(notes().set('communityName', name), notes().set('communityCreated', true), notes().set('errorMessage', null)); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts index 675ee96db..92911f16d 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts @@ -1,5 +1,6 @@ +import { TaskStep } from '@ocom-verification/verification-shared/serenity'; import { actors } from '@ocom-verification/verification-shared/test-data'; -import { type Actor, Interaction, the } from '@serenity-js/core'; +import { type Activity, type Actor, Task, the } from '@serenity-js/core'; import type { Page } from 'playwright'; import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; @@ -47,32 +48,34 @@ export async function performOAuth2Login(page: Page): Promise { } /** - * Screenplay Interaction — confirms the actor is authenticated. + * Screenplay Task — confirms the actor is authenticated. * * The browser context is pre-authenticated by {@link performOAuth2Login} - * during server setup. This interaction navigates to a protected route and + * during server setup. This task navigates to a protected route and * verifies the page loads without being kicked to the auth provider. */ export const OAuth2Login = (_email?: string, _password?: string) => - Interaction.where(the`#actor logs in via OAuth2`, async (serenityActor) => { - const actor = serenityActor as unknown as Actor; - const { page } = BrowseTheWeb.withActor(actor); + Task.where( + the`#actor logs in via OAuth2`, + new TaskStep('#actor confirms the OAuth2 session is active', async (actor) => { + const { page } = BrowseTheWeb.withActor(actor as Actor); - // Session tokens live in sessionStorage from pre-auth. - try { - await page.goto('/community/accounts', { - waitUntil: 'networkidle', - timeout: 30_000, - }); - } catch { - // Navigation may be interrupted by OIDC redirect on first access - } + // Session tokens live in sessionStorage from pre-auth. + try { + await page.goto('/community/accounts', { + waitUntil: 'networkidle', + timeout: 30_000, + }); + } catch { + // Navigation may be interrupted by OIDC redirect on first access + } - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', actors.CommunityOwner.email); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - } + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', actors.CommunityOwner.email); + await page.fill('input[name="password"]', 'password'); + await page.click('button[type="submit"]'); + } - await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); - }); + await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); + }) as Activity, + ); diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index 13c3822aa..9d4803c01 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -10,6 +10,7 @@ "./formatters": "./src/formatters/index.ts", "./servers": "./src/servers/index.ts", "./settings": "./src/settings/index.ts", + "./serenity": "./src/serenity/index.ts", "./pages": "./src/pages/index.ts", "./pages/jsdom": "./src/pages/adapters/jsdom-adapter.ts", "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts" @@ -20,6 +21,7 @@ "@ocom/service-mongoose": "workspace:*", "@cucumber/cucumber": "catalog:", "@cucumber/messages": "catalog:", + "@serenity-js/core": "catalog:", "@ocom/graphql": "workspace:*", "@ocom/application-services": "workspace:*", "@testing-library/react": "^16.3.0", diff --git a/packages/ocom-verification/verification-shared/src/serenity/index.ts b/packages/ocom-verification/verification-shared/src/serenity/index.ts new file mode 100644 index 000000000..54ba8c2e5 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/serenity/index.ts @@ -0,0 +1 @@ +export * from './task-step.ts'; diff --git a/packages/ocom-verification/verification-shared/src/serenity/task-step.ts b/packages/ocom-verification/verification-shared/src/serenity/task-step.ts new file mode 100644 index 000000000..b11ff0a82 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/serenity/task-step.ts @@ -0,0 +1,14 @@ +import { Task } from '@serenity-js/core'; + +export class TaskStep extends Task { + constructor( + description: string, + private readonly action: (actor: unknown) => Promise, + ) { + super(description); + } + + performAs(actor: unknown): Promise { + return this.action(actor); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68e8e6f12..3be5c052c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,7 +101,7 @@ catalogs: version: 4.1.2 overrides: - axios: 1.15.2 + axios: 1.16.0 follow-redirects: ^1.16.0 vite: 8.0.5 jiti: 2.6.1 @@ -1253,6 +1253,9 @@ importers: '@ocom/service-mongoose': specifier: workspace:* version: link:../../ocom/service-mongoose + '@serenity-js/core': + specifier: 'catalog:' + version: 3.42.2 '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -7222,8 +7225,8 @@ packages: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} - axios@1.15.2: - resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} azurite@3.35.0: resolution: {integrity: sha512-GzKmi+/5U0baNRjEEVtBMLpLuIKEJ0uSh0VWBzOI4qe4f5ziJyoZQmcTO7QhxZTF6+rphj7TZS3PtJY7uiiacA==} @@ -13347,18 +13350,6 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -14100,7 +14091,7 @@ snapshots: '@azure/ms-rest-js@1.11.2': dependencies: '@azure/core-auth': 1.10.1 - axios: 1.15.2 + axios: 1.16.0 form-data: 2.5.5 tough-cookie: 2.5.0 tslib: 1.14.1 @@ -18360,7 +18351,7 @@ snapshots: dependencies: '@serenity-js/core': 3.42.2 agent-base: 7.1.4 - axios: 1.15.2 + axios: 1.16.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 11.3.5 @@ -18375,7 +18366,7 @@ snapshots: '@serenity-js/core': 3.42.2 '@serenity-js/rest': 3.42.2 ansi-regex: 5.0.1 - axios: 1.15.2 + axios: 1.16.0 chalk: 4.1.2 find-java-home: 2.0.0 progress: 2.0.3 @@ -18426,7 +18417,7 @@ snapshots: '@sonar/scan@4.3.2': dependencies: adm-zip: 0.5.16 - axios: 1.15.2 + axios: 1.16.0 commander: 13.1.0 fs-extra: 11.3.2 hpagent: 1.2.0 @@ -19657,7 +19648,7 @@ snapshots: axe-core@4.11.0: {} - axios@1.15.2: + axios@1.16.0: dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -19670,7 +19661,7 @@ snapshots: '@azure/ms-rest-js': 1.11.2 applicationinsights: 2.9.8 args: 5.0.3 - axios: 1.15.2 + axios: 1.16.0 etag: 1.8.1 express: 4.22.2 fs-extra: 11.3.2 @@ -26870,8 +26861,6 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@7.5.10: {} - ws@8.20.1: {} wsl-utils@0.1.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cc09ee7ba..9238556fd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,7 +59,7 @@ allowBuilds: snyk: true overrides: - axios: 1.15.2 + axios: 1.16.0 follow-redirects: ^1.16.0 vite: "catalog:" jiti: 2.6.1 From ba948492cef937b36842ab471c445d8056097f9e Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 1 Jun 2026 16:30:57 -0400 Subject: [PATCH 13/40] initial commit for verify --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 886223cc6..a997ba1ed 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo ## Introduction + [Getting Started](https://developers.cellixjs.org/docs/intro): Our Docusaurus website will help you get started with running and contributing to CellixJS From 9be2b2ddc06c1fa70f091b7c1e47a8a499ca5888 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 2 Jun 2026 10:26:02 -0400 Subject: [PATCH 14/40] modularize serenity framework in a way that can be more easily used by consumers of cellix --- knip.json | 24 +- packages/cellix/serenity-framework/.gitignore | 1 + packages/cellix/serenity-framework/README.md | 131 +++++ .../cellix/serenity-framework/manifest.md | 64 +++ .../cellix/serenity-framework/package.json | 158 ++++++ .../src/clients/graphql-client.test.ts | 38 ++ .../src/clients/graphql-client.ts | 99 ++++ .../serenity-framework/src/clients/index.ts | 2 + .../src/cucumber/actor-name.ts | 22 + .../src/cucumber/cucumber.test.ts | 22 + .../src/cucumber/gherkin-data-table.ts | 29 + .../serenity-framework/src/cucumber/hooks.ts | 4 + .../serenity-framework/src/cucumber/index.ts | 7 + .../src/cucumber/lifecycle-hooks.ts | 47 ++ .../src/cucumber/screenshot-hooks.ts | 53 ++ .../src/cucumber/world.test.ts | 25 + .../serenity-framework/src/cucumber/world.ts | 88 +++ .../src/formatters/agent-formatter.ts | 44 +- .../cellix/serenity-framework/src/index.ts | 11 + .../src/infrastructure/api-infrastructure.ts | 244 +++++++++ .../src/infrastructure/api.ts | 11 + .../src/infrastructure/e2e-infrastructure.ts | 308 +++++++++++ .../src/infrastructure/e2e.ts | 9 + .../src/infrastructure/index.ts | 4 + .../src/infrastructure/infrastructure.test.ts | 158 ++++++ .../src/jsdom/asset-loader-hooks.ts | 54 ++ .../src/jsdom/css-module-types.d.ts | 4 + .../src/jsdom/css-modules.ts | 1 + .../serenity-framework/src/jsdom}/jsdom.d.ts | 0 .../src/jsdom/react-render.ts | 47 ++ .../src/jsdom/register-asset-loader.ts | 10 + .../serenity-framework/src/jsdom/setup.ts} | 0 .../src/pages/adapters/jsdom-adapter.ts | 117 ++-- .../src/pages/adapters/playwright-adapter.ts | 27 +- .../serenity-framework/src/pages/index.ts | 12 + .../src/pages/page-adapter.ts | 103 ++++ .../src/pages/page-object.test.ts | 21 + .../src/pages/page-object.ts | 38 ++ .../src/serenity}/browse-the-web.ts | 38 +- .../src/serenity/browser.ts | 1 + .../serenity-framework/src/serenity/cast.ts | 44 ++ .../serenity-framework/src/serenity/index.ts | 3 + .../src/serenity/task-step.test.ts | 16 + .../src/serenity/task-step.ts | 29 + .../src/servers/api-test-server.ts | 13 + .../src/servers/apollo-graphql-test-server.ts | 94 ++++ .../src/servers/auth-test-server.ts | 12 + .../src/servers/azurite-test-server.ts | 13 + .../serenity-framework/src/servers/index.ts | 18 + .../src/servers/mongo-memory-test-server.ts | 122 +++++ .../src/servers/process-environment.ts | 11 + .../src/servers/process-test-server.ts | 293 ++++++++++ .../src/servers/server-group.test.ts | 45 ++ .../src/servers/test-server-group.ts | 54 ++ .../src/servers/test-server.ts | 21 + .../src/servers/ui-portal-test-server.ts | 13 + .../serenity-framework/src/settings/index.ts | 2 + .../src/settings/settings.test.ts | 22 + .../src/settings/timeout-settings.ts | 93 ++++ .../cellix/serenity-framework/tsconfig.json | 12 + .../serenity-framework/tsconfig.vitest.json | 3 + packages/cellix/serenity-framework/turbo.json | 3 + .../serenity-framework/vitest.config.ts | 8 + .../acceptance-api/cucumber.js | 2 +- .../acceptance-api/package.json | 6 + .../header-types.ts => notes/header-notes.ts} | 0 .../step-definitions/header-login.steps.ts | 2 +- .../tasks/click-header-sign-in.ts | 4 +- .../community-notes.ts} | 0 .../community/questions/community-name.ts | 4 +- .../community/questions/community-status.ts | 2 +- .../create-community.steps.ts | 22 +- .../community/tasks/create-community.ts | 43 +- .../src/shared/abilities/create-community.ts | 47 ++ .../src/shared/abilities/graphql-client.ts | 48 +- .../src/shared/abilities/index.ts | 2 + .../application-services/index.ts | 0 .../mock-application-services.ts | 0 .../src/shared/cucumber-lifecycle-hooks.ts | 24 + .../src/shared/shared-infrastructure.ts | 39 ++ .../acceptance-api/src/shared/support/cast.ts | 10 - .../src/shared/support/domain-test-helpers.ts | 1 - .../src/shared/support/hooks.ts | 31 -- .../shared/support/shared-infrastructure.ts | 51 -- .../src/shared/test-server-factories.ts | 36 ++ .../acceptance-api/src/world.ts | 48 +- .../acceptance-ui/cucumber.js | 2 +- .../acceptance-ui/package.json | 8 +- .../header-types.ts => notes/header-notes.ts} | 0 .../step-definitions/header-login.steps.tsx | 7 +- .../tasks/click-header-sign-in.ts | 9 +- .../community-notes.ts} | 0 .../questions/community-created-flag.ts | 2 +- .../questions/community-error-message.ts | 2 +- .../community/questions/community-name.ts | 2 +- .../create-community.steps.tsx | 22 +- .../community/tasks/create-community.ts | 9 +- .../src/shared/cucumber-lifecycle-hooks.ts | 17 + .../src/shared/ocom-component-wrapper.ts | 13 + .../src/shared/page-contracts.ts | 5 + .../acceptance-ui/src/shared/support/cast.ts | 12 - .../acceptance-ui/src/shared/support/hooks.ts | 15 - .../shared/support/ui/asset-loader-hooks.mjs | 34 -- .../src/shared/support/ui/react-render.ts | 31 -- .../support/ui/register-asset-loader.ts | 10 - .../src/shared/support/ui/setup-jsdom.ts | 5 - .../src/step-definitions/index.ts | 4 +- .../acceptance-ui/src/world.ts | 25 +- .../acceptance-ui/tsconfig.json | 2 +- .../ocom-verification/e2e-tests/cucumber.js | 2 +- .../ocom-verification/e2e-tests/package.json | 1 + .../header-types.ts => notes/header-notes.ts} | 0 .../step-definitions/header-login.steps.ts | 4 +- .../tasks/click-header-sign-in.ts | 9 +- .../community-notes.ts} | 0 .../questions/community-created-flag.ts | 2 +- .../questions/community-error-message.ts | 2 +- .../community/questions/community-name.ts | 2 +- .../create-community.steps.ts | 14 +- .../community/tasks/create-community.ts | 11 +- .../src/shared/abilities/oauth2-login.ts | 108 ++++ .../src/shared/cucumber-lifecycle-hooks.ts | 24 + .../servers => environment}/app-paths.ts | 2 +- .../servers => environment}/dev-script.ts | 0 .../resolve-portless.ts | 2 +- .../shared/environment/test-environment.ts | 108 ++++ .../servers => environment}/worktree-ports.ts | 0 .../e2e-tests/src/shared/page-contracts.ts | 5 + .../src/shared/shared-infrastructure.ts | 60 ++ .../e2e-tests/src/shared/support/cast.ts | 13 - .../e2e-tests/src/shared/support/hooks.ts | 47 -- .../src/shared/support/oauth2-login.ts | 81 --- .../support/servers/child-process-env.ts | 4 - .../src/shared/support/servers/index.ts | 17 - .../shared/support/servers/portless-server.ts | 201 ------- .../shared/support/servers/test-api-server.ts | 66 --- .../support/servers/test-azurite-server.ts | 124 ----- .../servers/test-community-vite-server.ts | 46 -- .../support/servers/test-environment.ts | 70 --- .../support/servers/test-oauth2-server.ts | 37 -- .../support/servers/test-staff-vite-server.ts | 46 -- .../shared/support/shared-infrastructure.ts | 190 ------- .../src/shared/test-server-factories.ts | 108 ++++ .../e2e-tests/src/step-definitions/index.ts | 2 +- .../ocom-verification/e2e-tests/src/world.ts | 52 +- .../verification-shared/package.json | 29 +- .../src/abilities/create-community.ts | 53 ++ .../src/abilities/index.ts | 2 + .../src/formatters/index.ts | 1 - .../src/helpers/actor-helpers.ts | 12 - .../src/helpers/date-helpers.ts | 22 - .../src/helpers/gherkin-helpers.ts | 10 - .../verification-shared/src/helpers/index.ts | 10 - .../src/helpers/user-helpers.ts | 20 - .../src/pages/community.page.ts | 62 +-- .../src/pages/home.page.ts | 11 +- .../verification-shared/src/pages/index.ts | 16 - .../src/pages/login.page.ts | 34 -- .../src/pages/page-adapter.ts | 38 -- .../community.page-interface.ts | 5 - .../page-interfaces/home.page-interface.ts | 5 - .../src/pages/page-interfaces/index.ts | 12 - .../page-interfaces/login.page-interface.ts | 5 - .../verification-shared/src/serenity/index.ts | 1 - .../src/serenity/task-step.ts | 14 - .../src/servers/graphql-test-server.ts | 111 ---- .../verification-shared/src/servers/index.ts | 8 - .../src/servers/test-mongodb-server.ts | 130 ----- .../src/servers/test-server.interface.ts | 18 - .../verification-shared/src/settings/index.ts | 2 - .../src/settings/portless-settings.ts | 58 -- .../src/settings/timeout-settings.ts | 57 -- .../src/test-data/seed/end-users.ts | 5 +- .../src/test-data/test-actors.ts | 10 +- .../src/test-data/utils.ts | 4 +- pnpm-lock.yaml | 513 ++++++++++-------- pnpm-workspace.yaml | 6 +- 177 files changed, 4057 insertions(+), 2365 deletions(-) create mode 100644 packages/cellix/serenity-framework/.gitignore create mode 100644 packages/cellix/serenity-framework/README.md create mode 100644 packages/cellix/serenity-framework/manifest.md create mode 100644 packages/cellix/serenity-framework/package.json create mode 100644 packages/cellix/serenity-framework/src/clients/graphql-client.test.ts create mode 100644 packages/cellix/serenity-framework/src/clients/graphql-client.ts create mode 100644 packages/cellix/serenity-framework/src/clients/index.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/actor-name.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/cucumber.test.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/gherkin-data-table.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/hooks.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/index.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/lifecycle-hooks.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/screenshot-hooks.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/world.test.ts create mode 100644 packages/cellix/serenity-framework/src/cucumber/world.ts rename packages/{ocom-verification/verification-shared => cellix/serenity-framework}/src/formatters/agent-formatter.ts (77%) create mode 100644 packages/cellix/serenity-framework/src/index.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/api.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/e2e.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/index.ts create mode 100644 packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts create mode 100644 packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts create mode 100644 packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts create mode 100644 packages/cellix/serenity-framework/src/jsdom/css-modules.ts rename packages/{ocom-verification/acceptance-ui/src/shared/support/ui => cellix/serenity-framework/src/jsdom}/jsdom.d.ts (100%) create mode 100644 packages/cellix/serenity-framework/src/jsdom/react-render.ts create mode 100644 packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts rename packages/{ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts => cellix/serenity-framework/src/jsdom/setup.ts} (100%) rename packages/{ocom-verification/verification-shared => cellix/serenity-framework}/src/pages/adapters/jsdom-adapter.ts (57%) rename packages/{ocom-verification/verification-shared => cellix/serenity-framework}/src/pages/adapters/playwright-adapter.ts (76%) create mode 100644 packages/cellix/serenity-framework/src/pages/index.ts create mode 100644 packages/cellix/serenity-framework/src/pages/page-adapter.ts create mode 100644 packages/cellix/serenity-framework/src/pages/page-object.test.ts create mode 100644 packages/cellix/serenity-framework/src/pages/page-object.ts rename packages/{ocom-verification/e2e-tests/src/shared/abilities => cellix/serenity-framework/src/serenity}/browse-the-web.ts (61%) create mode 100644 packages/cellix/serenity-framework/src/serenity/browser.ts create mode 100644 packages/cellix/serenity-framework/src/serenity/cast.ts create mode 100644 packages/cellix/serenity-framework/src/serenity/index.ts create mode 100644 packages/cellix/serenity-framework/src/serenity/task-step.test.ts create mode 100644 packages/cellix/serenity-framework/src/serenity/task-step.ts create mode 100644 packages/cellix/serenity-framework/src/servers/api-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/apollo-graphql-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/auth-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/azurite-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/index.ts create mode 100644 packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/process-environment.ts create mode 100644 packages/cellix/serenity-framework/src/servers/process-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/server-group.test.ts create mode 100644 packages/cellix/serenity-framework/src/servers/test-server-group.ts create mode 100644 packages/cellix/serenity-framework/src/servers/test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/settings/index.ts create mode 100644 packages/cellix/serenity-framework/src/settings/settings.test.ts create mode 100644 packages/cellix/serenity-framework/src/settings/timeout-settings.ts create mode 100644 packages/cellix/serenity-framework/tsconfig.json create mode 100644 packages/cellix/serenity-framework/tsconfig.vitest.json create mode 100644 packages/cellix/serenity-framework/turbo.json create mode 100644 packages/cellix/serenity-framework/vitest.config.ts rename packages/ocom-verification/acceptance-api/src/contexts/authentication/{abilities/header-types.ts => notes/header-notes.ts} (100%) rename packages/ocom-verification/acceptance-api/src/contexts/community/{abilities/community-types.ts => notes/community-notes.ts} (100%) create mode 100644 packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts create mode 100644 packages/ocom-verification/acceptance-api/src/shared/abilities/index.ts rename packages/ocom-verification/acceptance-api/src/shared/{support => }/application-services/index.ts (100%) rename packages/ocom-verification/acceptance-api/src/shared/{support => }/application-services/mock-application-services.ts (100%) create mode 100644 packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts create mode 100644 packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/support/cast.ts delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/support/domain-test-helpers.ts delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts create mode 100644 packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts rename packages/ocom-verification/acceptance-ui/src/contexts/authentication/{abilities/header-types.ts => notes/header-notes.ts} (100%) rename packages/ocom-verification/acceptance-ui/src/contexts/community/{abilities/community-types.ts => notes/community-notes.ts} (100%) create mode 100644 packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/shared/ocom-component-wrapper.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/shared/page-contracts.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/cast.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/asset-loader-hooks.mjs delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/register-asset-loader.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/support/ui/setup-jsdom.ts rename packages/ocom-verification/e2e-tests/src/contexts/authentication/{abilities/header-types.ts => notes/header-notes.ts} (100%) rename packages/ocom-verification/e2e-tests/src/contexts/community/{abilities/community-types.ts => notes/community-notes.ts} (100%) create mode 100644 packages/ocom-verification/e2e-tests/src/shared/abilities/oauth2-login.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts rename packages/ocom-verification/e2e-tests/src/shared/{support/servers => environment}/app-paths.ts (86%) rename packages/ocom-verification/e2e-tests/src/shared/{support/servers => environment}/dev-script.ts (100%) rename packages/ocom-verification/e2e-tests/src/shared/{support/servers => environment}/resolve-portless.ts (89%) create mode 100644 packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts rename packages/ocom-verification/e2e-tests/src/shared/{support/servers => environment}/worktree-ports.ts (100%) create mode 100644 packages/ocom-verification/e2e-tests/src/shared/page-contracts.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/cast.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts create mode 100644 packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts create mode 100644 packages/ocom-verification/verification-shared/src/abilities/create-community.ts create mode 100644 packages/ocom-verification/verification-shared/src/abilities/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/formatters/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/helpers/actor-helpers.ts delete mode 100644 packages/ocom-verification/verification-shared/src/helpers/date-helpers.ts delete mode 100644 packages/ocom-verification/verification-shared/src/helpers/gherkin-helpers.ts delete mode 100644 packages/ocom-verification/verification-shared/src/helpers/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/helpers/user-helpers.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/login.page.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/page-adapter.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/pages/page-interfaces/login.page-interface.ts delete mode 100644 packages/ocom-verification/verification-shared/src/serenity/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/serenity/task-step.ts delete mode 100644 packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts delete mode 100644 packages/ocom-verification/verification-shared/src/servers/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts delete mode 100644 packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/index.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/portless-settings.ts delete mode 100644 packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts diff --git a/knip.json b/knip.json index 130ffcb7f..859113a5c 100644 --- a/knip.json +++ b/knip.json @@ -24,6 +24,11 @@ "project": ["src/**/*.ts"], "ignore": ["**/graphql-tools-scalars.ts"] }, + "packages/cellix/serenity-framework": { + "project": ["src/**/*.ts"], + "ignore": ["**/graphql-tools-scalars.ts"], + "ignoreDependencies": ["@testing-library/react", "playwright", "react"] + }, "packages/cellix/mongoose-seedwork": { "entry": ["src/index.ts"], "project": ["src/**/*.ts"], @@ -62,20 +67,11 @@ "project": ["src/**/*.{ts,cjs}"] }, "packages/ocom-verification/verification-shared": { - "entry": [ - "src/formatters/index.ts", - "src/helpers/index.ts", - "src/pages/index.ts", - "src/pages/adapters/jsdom-adapter.ts", - "src/pages/adapters/playwright-adapter.ts", - "src/servers/index.ts", - "src/settings/index.ts", - "src/test-data/index.ts" - ], + "entry": ["src/abilities/index.ts", "src/pages/index.ts", "src/test-data/index.ts"], "project": ["src/**/*.ts"] }, "packages/ocom-verification/acceptance-api": { - "entry": ["cucumber.js", "src/world.ts", "src/step-definitions/index.ts"], + "entry": ["cucumber.js", "src/world.ts", "src/step-definitions/index.ts", "src/shared/abilities/index.ts"], "project": ["src/**/*.ts"], "ignoreBinaries": ["report"], "ignoreUnresolved": ["progress-bar"] @@ -83,11 +79,11 @@ "packages/ocom-verification/acceptance-ui": { "entry": ["cucumber.js", "src/world.ts", "src/step-definitions/index.ts"], "project": ["src/**/*.{ts,tsx,mjs}"], - "ignoreUnresolved": ["progress-bar"], - "ignore": ["src/shared/support/ui/**"] + "ignore": ["src/shared/support/ui/**"], + "ignoreUnresolved": ["progress-bar"] }, "packages/ocom-verification/e2e-tests": { - "entry": ["cucumber.js", "src/world.ts", "src/contexts/**/step-definitions/**/*.steps.ts", "src/shared/support/**/*.ts"], + "entry": ["cucumber.js", "src/world.ts", "src/contexts/**/step-definitions/**/*.steps.ts", "src/shared/environment/**/*.ts", "src/shared/abilities/**/*.ts", "src/shared/test-server-factories.ts"], "project": ["src/**/*.ts"], "ignoreUnresolved": ["progress-bar"] }, diff --git a/packages/cellix/serenity-framework/.gitignore b/packages/cellix/serenity-framework/.gitignore new file mode 100644 index 000000000..53c37a166 --- /dev/null +++ b/packages/cellix/serenity-framework/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md new file mode 100644 index 000000000..8490ef0e5 --- /dev/null +++ b/packages/cellix/serenity-framework/README.md @@ -0,0 +1,131 @@ +# @cellix/serenity-framework + +Reusable Serenity/JS verification framework primitives for Cellix packages. + +This package is intentionally app-agnostic. It provides adapters, a generic Serenity cast, Cucumber utilities, managed worlds, and server lifecycle infrastructure; consumers provide page objects, selectors, app paths, schemas, services, seed data, and environment-specific values. + +## Page adapters + +Page objects should depend on `PageAdapter`, not directly on jsdom or Playwright: + +```ts +import { AdapterBackedPageObject, type PageAdapter } from '@cellix/serenity-framework/pages'; + +class CommunityPage extends AdapterBackedPageObject { + constructor(adapter: PageAdapter) { + super(adapter); + } + + async createCommunity(name: string): Promise { + await this.adapter.getByPlaceholder('Name').fill(name); + await this.adapter.getByRole('button', { name: /Create/i }).click(); + } +} +``` + +Use the runtime-specific adapter at the edge of the test package: + +```ts +import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwright'; +``` + +## Server composition + +Use generic server descriptors for app-specific processes, then load them into the suite infrastructure manager instead of hand-writing suite startup code. + +```ts +import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; +import { ApiTestServer, AuthTestServer, AzuriteTestServer, UiPortalTestServer } from '@cellix/serenity-framework/servers'; + +const communityPortal = new UiPortalTestServer({ + portalName: 'community', + cwd: '/repo/apps/ui-community', + getUrl: () => 'https://community.localhost:1355', + spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], +}); + +export const infrastructure = E2EInfrastructure + .using({ + mongoServer: { dbName, port, replSetName, seedData }, + azuriteServer, + authServer, + createApiServer: ({ getMongoConnectionString }) => + new ApiTestServer({ + serverName: 'Api', + executable: 'pnpm', + spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], + cwd: '/repo/apps/api', + extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: getMongoConnectionString() }), + getUrl: () => 'https://api.localhost:1355/api/graphql', + readyMarker: 'Functions:', + }), + launchBrowser: () => playwright.chromium.launch({ headless: true }), + }) + .addUiPortal('community', communityPortal) + .addUiPortal('staff', staffPortal); + +await infrastructure.ensureStarted(); +await infrastructure.resetScenarioState(); +await infrastructure.stopAll(); +``` + +The framework never imports app paths or app-specific environment helpers. Pass those values into descriptors from the consumer package. + +## API acceptance infrastructure + +API-only acceptance suites use the smaller infrastructure manager. It requires MongoDB options and an API server factory, and owns startup, URL state, reset, and shutdown. If the API needs a Mongoose service, pass a factory that returns the consumer's Mongoose-compatible service; the framework starts it, clears registered models, and stops it. + +```ts +import { ApiInfrastructure } from '@cellix/serenity-framework/infrastructure/api'; +import { ApolloGraphQLTestServer } from '@cellix/serenity-framework/servers'; + +export const infrastructure = ApiInfrastructure.using({ + mongoServer: { dbName, port, replSetName, seedData }, + mongoose: { + createService: (connectionString) => createMongooseService(connectionString), + }, + createApiServer: ({ getMongooseService }) => + new ApolloGraphQLTestServer({ + schema, + context: () => createContext(getMongooseService()), + }), +}); +``` + +## Managed worlds + +Use a managed world when the suite does not need custom Cucumber world methods. The framework starts infrastructure, validates state, engages the cast, and resets scenario state. + +```ts +import { GraphQLClient } from '@cellix/serenity-framework/clients/graphql'; +import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; + +export const ApiWorld = registerManagedSerenityWorld({ + infrastructure, + validateState: (state) => { + if (!state.apiUrl) throw new Error('API URL was not initialized'); + }, + createCast: (state) => + new SerenityCast({ + useNotepad: true, + abilities: [() => new GraphQLClient({ apiUrl: state.apiUrl ?? '' })], + }), +}); +``` + +## jsdom helpers + +Component-level acceptance tests can import framework jsdom setup and asset-loader hooks instead of carrying per-suite copies: + +```sh +NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js +``` + +```ts +import '@cellix/serenity-framework/jsdom/setup'; +/// Include `@cellix/serenity-framework/src/jsdom/css-module-types.d.ts` +/// in tsconfig when component imports include CSS modules. +import { mountComponent, unmountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +``` diff --git a/packages/cellix/serenity-framework/manifest.md b/packages/cellix/serenity-framework/manifest.md new file mode 100644 index 000000000..11d6563c4 --- /dev/null +++ b/packages/cellix/serenity-framework/manifest.md @@ -0,0 +1,64 @@ +# manifest.md - @cellix/serenity-framework + +## Purpose + +Provide reusable Serenity/JS, Cucumber, page-adapter, and test-server framework primitives that Cellix consumers can compose with app-specific pages, step definitions, services, URLs, and data. + +## Scope + +This package owns generic verification infrastructure only: + +- Serenity task, cast, and browser-ability primitives +- Cucumber data-table, lifecycle, screenshot, and managed-world helpers +- Runtime-agnostic page adapter contracts and jsdom/Playwright adapter implementations +- jsdom globals, CSS module declarations, asset-loader hooks, and generic React render helpers for component acceptance tests +- Adapter-backed page-object base contracts +- Timeout utilities +- Configurable process, UI portal, Apollo GraphQL, Azurite, Mongo memory, and server-group lifecycle utilities +- API acceptance and browser E2E infrastructure managers that create MongoDB from options and compose consumer-owned server factories + +## Non-goals + +- OCOM-specific page objects, selectors, scenarios, seed data, application services, GraphQL schemas, app paths, or environment variable names +- Opinionated Cucumber step definitions +- Production server orchestration + +## Public API shape + +- `@cellix/serenity-framework/serenity`: `TaskStep` +- `@cellix/serenity-framework/cucumber`: `ActorName`, `GherkinDataTable`, lifecycle hook helpers +- `@cellix/serenity-framework/cucumber/screenshot`: browser screenshot-on-failure hook helpers +- `@cellix/serenity-framework/pages`: adapter contracts and page-object base types +- `@cellix/serenity-framework/pages/jsdom`: `JsdomPageAdapter` +- `@cellix/serenity-framework/pages/playwright`: `PlaywrightPageAdapter` +- `@cellix/serenity-framework/clients/graphql`: `GraphQLClient` +- `@cellix/serenity-framework/jsdom/setup`: jsdom global bootstrap side-effect module +- `@cellix/serenity-framework/jsdom/register-asset-loader`: jsdom asset-loader registration side-effect module +- `@cellix/serenity-framework/jsdom/react-render`: generic React mount/unmount helpers +- `@cellix/serenity-framework/jsdom/css-modules`: package-owned CSS module declaration target +- `@cellix/serenity-framework/serenity`: `TaskStep`, `SerenityCast` +- `@cellix/serenity-framework/serenity/browser`: `BrowseTheWeb` +- `@cellix/serenity-framework/infrastructure/api`: API acceptance infrastructure manager with MongoDB options, optional Mongoose service management, and an API server factory +- `@cellix/serenity-framework/infrastructure/e2e`: browser E2E infrastructure manager with MongoDB options, required Azurite/auth servers, an API server factory, and chainable UI portals +- `@cellix/serenity-framework/servers`: generic server lifecycle classes and interfaces +- `@cellix/serenity-framework/settings`: timeout helpers + +## Package boundaries + +The package must not import from `@ocom/*`, `@ocom-verification/*`, `apps/*`, or local OCOM path helpers. Consumers pass app-specific values through options objects, descriptors, factories, or callbacks. + +## Dependencies / relationships + +Downstream consumers in this monorepo are expected to include `@ocom-verification/acceptance-api`, `@ocom-verification/acceptance-ui`, and `@ocom-verification/e2e-tests`. + +## Testing strategy + +Prefer public-entrypoint tests that exercise observable behavior through sectioned exports. Do not test private implementation details or deep-import package internals. + +## Documentation obligations + +Keep `README.md` consumer-facing and package-centric. Meaningful public exports require TSDoc that explains purpose, options, return values, side effects, errors, and usage where helpful. + +## Release-readiness standards + +Build and test this package plus affected verification consumers before treating the package as ready for external npm use. Any public export removal or behavioral incompatibility requires explicit semver review. diff --git a/packages/cellix/serenity-framework/package.json b/packages/cellix/serenity-framework/package.json new file mode 100644 index 000000000..fe50b41f5 --- /dev/null +++ b/packages/cellix/serenity-framework/package.json @@ -0,0 +1,158 @@ +{ + "name": "@cellix/serenity-framework", + "version": "1.0.0", + "description": "Reusable Serenity/JS verification framework primitives for Cellix packages", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src/jsdom/css-module-types.d.ts" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./clients/graphql": { + "types": "./dist/clients/graphql-client.d.ts", + "default": "./dist/clients/graphql-client.js" + }, + "./cucumber": { + "types": "./dist/cucumber/index.d.ts", + "default": "./dist/cucumber/index.js" + }, + "./cucumber/actor-name": { + "types": "./dist/cucumber/actor-name.d.ts", + "default": "./dist/cucumber/actor-name.js" + }, + "./cucumber/gherkin-data-table": { + "types": "./dist/cucumber/gherkin-data-table.d.ts", + "default": "./dist/cucumber/gherkin-data-table.js" + }, + "./cucumber/hooks": { + "types": "./dist/cucumber/hooks.d.ts", + "default": "./dist/cucumber/hooks.js" + }, + "./cucumber/screenshot": { + "types": "./dist/cucumber/screenshot-hooks.d.ts", + "default": "./dist/cucumber/screenshot-hooks.js" + }, + "./formatters/agent": { + "types": "./dist/formatters/agent-formatter.d.ts", + "default": "./dist/formatters/agent-formatter.js" + }, + "./infrastructure": { + "types": "./dist/infrastructure/index.d.ts", + "default": "./dist/infrastructure/index.js" + }, + "./infrastructure/api": { + "types": "./dist/infrastructure/api.d.ts", + "default": "./dist/infrastructure/api.js" + }, + "./infrastructure/e2e": { + "types": "./dist/infrastructure/e2e.d.ts", + "default": "./dist/infrastructure/e2e.js" + }, + "./pages": { + "types": "./dist/pages/index.d.ts", + "default": "./dist/pages/index.js" + }, + "./pages/jsdom": { + "types": "./dist/pages/adapters/jsdom-adapter.d.ts", + "default": "./dist/pages/adapters/jsdom-adapter.js" + }, + "./pages/playwright": { + "types": "./dist/pages/adapters/playwright-adapter.d.ts", + "default": "./dist/pages/adapters/playwright-adapter.js" + }, + "./jsdom/setup": { + "types": "./dist/jsdom/setup.d.ts", + "default": "./dist/jsdom/setup.js" + }, + "./jsdom/register-asset-loader": { + "types": "./dist/jsdom/register-asset-loader.d.ts", + "default": "./dist/jsdom/register-asset-loader.js" + }, + "./jsdom/asset-loader-hooks": { + "types": "./dist/jsdom/asset-loader-hooks.d.ts", + "default": "./dist/jsdom/asset-loader-hooks.js" + }, + "./jsdom/react-render": { + "types": "./dist/jsdom/react-render.d.ts", + "default": "./dist/jsdom/react-render.js" + }, + "./jsdom/css-modules": { + "types": "./src/jsdom/css-module-types.d.ts", + "default": "./dist/jsdom/css-modules.js" + }, + "./serenity": { + "types": "./dist/serenity/index.d.ts", + "default": "./dist/serenity/index.js" + }, + "./serenity/browser": { + "types": "./dist/serenity/browser.d.ts", + "default": "./dist/serenity/browser.js" + }, + "./servers": { + "types": "./dist/servers/index.d.ts", + "default": "./dist/servers/index.js" + }, + "./settings": { + "types": "./dist/settings/index.d.ts", + "default": "./dist/settings/index.js" + } + }, + "scripts": { + "prebuild": "pnpm run lint", + "build": "tsgo --build", + "clean": "rimraf dist tsconfig.tsbuildinfo && tsgo --build --clean", + "lint": "biome lint", + "format": "biome format --write", + "format:check": "biome format .", + "test": "vitest run --silent --reporter=dot", + "test:coverage": "vitest run --coverage --silent --reporter=dot", + "test:watch": "vitest" + }, + "dependencies": { + "@apollo/server": "catalog:", + "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", + "@cucumber/cucumber": "catalog:", + "@cucumber/messages": "catalog:", + "@serenity-js/core": "catalog:", + "graphql": "catalog:", + "graphql-depth-limit": "^1.1.0", + "jsdom": "catalog:", + "mongodb": "catalog:" + }, + "peerDependencies": { + "@testing-library/react": ">=16.0.0", + "playwright": ">=1.50.0", + "react": ">=18.0.0" + }, + "peerDependenciesMeta": { + "@testing-library/react": { + "optional": true + }, + "playwright": { + "optional": true + }, + "react": { + "optional": true + } + }, + "devDependencies": { + "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", + "@testing-library/react": "^16.3.0", + "@types/graphql-depth-limit": "^1.1.0", + "@types/node": "catalog:", + "@types/react": "^19.1.8", + "@vitest/coverage-istanbul": "catalog:", + "playwright": "catalog:", + "react": "^19.1.0", + "rimraf": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/cellix/serenity-framework/src/clients/graphql-client.test.ts b/packages/cellix/serenity-framework/src/clients/graphql-client.test.ts new file mode 100644 index 000000000..67ee4081f --- /dev/null +++ b/packages/cellix/serenity-framework/src/clients/graphql-client.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from 'vitest'; +import { GraphQLClient } from './index.ts'; + +describe('GraphQLClient', () => { + it('posts GraphQL operations with configured headers', async () => { + const fetcher = vi.fn(async () => Response.json({ data: { ok: true } })); + const client = new GraphQLClient({ + apiUrl: 'https://api.example.test/graphql', + fetch: fetcher as typeof fetch, + headers: { Authorization: 'Bearer test' }, + }); + + const response = await client.execute<{ ok: boolean }>('query Test { ok }', { id: 1 }); + + expect(response.data.ok).toBe(true); + expect(fetcher).toHaveBeenCalledWith( + 'https://api.example.test/graphql', + expect.objectContaining({ + body: JSON.stringify({ query: 'query Test { ok }', variables: { id: 1 } }), + headers: { + Authorization: 'Bearer test', + 'Content-Type': 'application/json', + }, + method: 'POST', + }), + ); + }); + + it('throws when GraphQL errors are returned', async () => { + const fetcher = vi.fn(async () => Response.json({ data: {}, errors: [{ message: 'Nope' }] })); + const client = new GraphQLClient({ + apiUrl: 'https://api.example.test/graphql', + fetch: fetcher as typeof fetch, + }); + + await expect(client.execute('query Test { ok }')).rejects.toThrow('Nope'); + }); +}); diff --git a/packages/cellix/serenity-framework/src/clients/graphql-client.ts b/packages/cellix/serenity-framework/src/clients/graphql-client.ts new file mode 100644 index 000000000..49d851451 --- /dev/null +++ b/packages/cellix/serenity-framework/src/clients/graphql-client.ts @@ -0,0 +1,99 @@ +import { Ability } from '@serenity-js/core'; + +/** GraphQL error shape returned by common GraphQL HTTP servers. */ +export interface GraphQLResponseError { + /** Human-readable error message. */ + message: string; +} + +/** Result returned from {@link GraphQLClient.execute}. */ +export interface GraphQLResponse = Record> { + /** GraphQL response data. */ + data: TData; + + /** Optional GraphQL errors returned by the server. */ + errors?: GraphQLResponseError[]; +} + +/** Options used to create a GraphQL Serenity ability. */ +export interface GraphQLClientOptions { + /** GraphQL HTTP endpoint URL. */ + apiUrl: string; + + /** Headers applied to every request. */ + headers?: Record | (() => Record); + + /** Fetch implementation. Defaults to `globalThis.fetch`. */ + fetch?: typeof fetch; +} + +/** + * Serenity ability for executing GraphQL operations over HTTP. + * + * Consumers provide the endpoint and any app-specific headers, such as test + * authorization tokens. GraphQL errors are raised as JavaScript `Error`s so + * Screenplay questions and tasks fail the scenario clearly. + */ +export class GraphQLClient extends Ability { + private readonly apiUrl: string; + private readonly fetcher: typeof fetch; + private readonly headers: Record | (() => Record) | undefined; + + /** + * @param options Endpoint, headers, and optional fetch implementation. + */ + constructor(options: GraphQLClientOptions) { + super(); + this.apiUrl = options.apiUrl; + this.headers = options.headers; + this.fetcher = options.fetch ?? globalThis.fetch; + } + + /** + * Create a GraphQL ability for a specific endpoint. + * + * @param apiUrl GraphQL HTTP endpoint URL. + * @param headers Optional static or lazy headers applied to each request. + */ + static at(apiUrl: string, headers?: Record | (() => Record)): GraphQLClient { + return new GraphQLClient({ apiUrl, ...(headers && { headers }) }); + } + + /** + * Execute a GraphQL query or mutation. + * + * @param query GraphQL document text. + * @param variables Variables supplied to the operation. + * @throws Error when the HTTP response is not OK or the GraphQL result contains errors. + */ + async execute = Record>(query: string, variables: Record = {}): Promise> { + const response = await this.fetcher(this.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.resolveHeaders(), + }, + body: JSON.stringify({ query, variables }), + }); + + const result = (await response.json()) as GraphQLResponse; + + if (result.errors?.length) { + throw new Error(result.errors.map((error) => error.message ?? 'Unknown error').join('; ')); + } + + if (!response.ok) { + throw new Error(`GraphQL error: ${response.status} ${response.statusText}`); + } + + return result; + } + + private resolveHeaders(): Record { + if (!this.headers) { + return {}; + } + + return typeof this.headers === 'function' ? this.headers() : this.headers; + } +} diff --git a/packages/cellix/serenity-framework/src/clients/index.ts b/packages/cellix/serenity-framework/src/clients/index.ts new file mode 100644 index 000000000..c30e09bc1 --- /dev/null +++ b/packages/cellix/serenity-framework/src/clients/index.ts @@ -0,0 +1,2 @@ +export type { GraphQLClientOptions, GraphQLResponse, GraphQLResponseError } from './graphql-client.ts'; +export { GraphQLClient } from './graphql-client.ts'; diff --git a/packages/cellix/serenity-framework/src/cucumber/actor-name.ts b/packages/cellix/serenity-framework/src/cucumber/actor-name.ts new file mode 100644 index 000000000..1cc4dba15 --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/actor-name.ts @@ -0,0 +1,22 @@ +/** Options used when resolving actor references from Gherkin text. */ +export interface ActorNameResolutionOptions { + /** Name used when a pronoun is supplied. Defaults to `Alice`. */ + defaultName?: string; +} + +const pronounPattern = /^(she|he|they)$/i; + +/** + * Resolver object for actor names found in Gherkin steps. + */ +export const ActorName = { + /** + * Resolve pronouns such as `she`, `he`, or `they` to a default actor name. + * + * @param actorName Name or pronoun captured from a Gherkin step. + * @param options Optional default name configuration. + */ + resolve(actorName: string, options: ActorNameResolutionOptions = {}): string { + return pronounPattern.test(actorName) ? (options.defaultName ?? 'Alice') : actorName; + }, +} as const; diff --git a/packages/cellix/serenity-framework/src/cucumber/cucumber.test.ts b/packages/cellix/serenity-framework/src/cucumber/cucumber.test.ts new file mode 100644 index 000000000..a800484fc --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/cucumber.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { ActorName, GherkinDataTable } from './index.ts'; + +describe('ActorName', () => { + it('resolves pronouns to the configured default actor name', () => { + expect(ActorName.resolve('she', { defaultName: 'Casey' })).toBe('Casey'); + expect(ActorName.resolve('Morgan', { defaultName: 'Casey' })).toBe('Morgan'); + }); +}); + +describe('GherkinDataTable', () => { + it('returns a typed rows hash', () => { + const dataTable = { + rowsHash: () => ({ name: 'Evergreen', status: 'Active' }), + }; + + const row = GherkinDataTable.from(dataTable as never).rowsHash<{ name: string; status: string }>(); + + expect(row.name).toBe('Evergreen'); + expect(row.status).toBe('Active'); + }); +}); diff --git a/packages/cellix/serenity-framework/src/cucumber/gherkin-data-table.ts b/packages/cellix/serenity-framework/src/cucumber/gherkin-data-table.ts new file mode 100644 index 000000000..dd084aa0a --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/gherkin-data-table.ts @@ -0,0 +1,29 @@ +import type { DataTable } from '@cucumber/cucumber'; + +/** + * Typed wrapper around a Cucumber `DataTable`. + */ +export class GherkinDataTable { + /** + * @param dataTable Cucumber data table received by a step definition. + */ + constructor(private readonly dataTable: DataTable) {} + + /** + * Return `rowsHash()` as a caller-provided object shape. + * + * @typeParam T Shape expected by the step definition. + */ + rowsHash(): T { + return this.dataTable.rowsHash() as T; + } + + /** + * Wrap a Cucumber data table. + * + * @param dataTable Cucumber data table received by a step definition. + */ + static from(dataTable: DataTable): GherkinDataTable { + return new GherkinDataTable(dataTable); + } +} diff --git a/packages/cellix/serenity-framework/src/cucumber/hooks.ts b/packages/cellix/serenity-framework/src/cucumber/hooks.ts new file mode 100644 index 000000000..bcf61416b --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/hooks.ts @@ -0,0 +1,4 @@ +export type { WorldLifecycleHooks } from './lifecycle-hooks.ts'; +export { registerWorldLifecycleHooks } from './lifecycle-hooks.ts'; +export type { ScreenshotOnFailureOptions } from './screenshot-hooks.ts'; +export { registerScreenshotOnFailureHook } from './screenshot-hooks.ts'; diff --git a/packages/cellix/serenity-framework/src/cucumber/index.ts b/packages/cellix/serenity-framework/src/cucumber/index.ts new file mode 100644 index 000000000..8f284ebb2 --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/index.ts @@ -0,0 +1,7 @@ +export type { ActorNameResolutionOptions } from './actor-name.ts'; +export { ActorName } from './actor-name.ts'; +export { GherkinDataTable } from './gherkin-data-table.ts'; +export type { WorldLifecycleHooks } from './lifecycle-hooks.ts'; +export { registerWorldLifecycleHooks } from './lifecycle-hooks.ts'; +export type { ManagedSerenityWorldInfrastructure, ManagedSerenityWorldOptions } from './world.ts'; +export { createManagedSerenityWorldClass, ManagedSerenityWorld, registerManagedSerenityWorld } from './world.ts'; diff --git a/packages/cellix/serenity-framework/src/cucumber/lifecycle-hooks.ts b/packages/cellix/serenity-framework/src/cucumber/lifecycle-hooks.ts new file mode 100644 index 000000000..25ed667eb --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/lifecycle-hooks.ts @@ -0,0 +1,47 @@ +import { After, AfterAll, Before, type ITestCaseHookParameter, type IWorld, setDefaultTimeout } from '@cucumber/cucumber'; + +/** Lifecycle callbacks used by {@link registerWorldLifecycleHooks}. */ +export interface WorldLifecycleHooks { + /** Scenario timeout in milliseconds. */ + scenarioTimeout?: number; + + /** Optional timeout for the before hook. */ + beforeTimeout?: number; + + /** Optional timeout for the after hook. */ + afterTimeout?: number; + + /** Called before each scenario. */ + before?: (world: TWorld) => Promise | void; + + /** Called after each scenario. */ + after?: (world: TWorld, parameter: ITestCaseHookParameter) => Promise | void; + + /** Called once after all scenarios complete. */ + afterAll?: () => Promise | void; +} + +/** + * Register common Cucumber world lifecycle hooks. + * + * @param hooks Hook callbacks and timeouts for a test package. + */ +export function registerWorldLifecycleHooks(hooks: WorldLifecycleHooks): void { + if (hooks.scenarioTimeout) { + setDefaultTimeout(hooks.scenarioTimeout); + } + + Before(hooks.beforeTimeout === undefined ? {} : { timeout: hooks.beforeTimeout }, async function (this: IWorld) { + await hooks.before?.(this as TWorld); + }); + + After(hooks.afterTimeout === undefined ? {} : { timeout: hooks.afterTimeout }, async function (this: IWorld, parameter: ITestCaseHookParameter) { + await hooks.after?.(this as TWorld, parameter); + }); + + if (hooks.afterAll) { + AfterAll(async () => { + await hooks.afterAll?.(); + }); + } +} diff --git a/packages/cellix/serenity-framework/src/cucumber/screenshot-hooks.ts b/packages/cellix/serenity-framework/src/cucumber/screenshot-hooks.ts new file mode 100644 index 000000000..3743da140 --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/screenshot-hooks.ts @@ -0,0 +1,53 @@ +import { mkdirSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { After, type ITestCaseHookParameter, type IWorld, Status } from '@cucumber/cucumber'; +import { BrowseTheWeb } from '../serenity/browse-the-web.ts'; + +/** Options used by {@link registerScreenshotOnFailureHook}. */ +export interface ScreenshotOnFailureOptions { + /** Directory where screenshots should be written. */ + reportsDir: string; + + /** Whether to capture screenshots on failure. Defaults to `false`. */ + enabled?: boolean; + + /** Ability provider. Defaults to `BrowseTheWeb.current()`. */ + getBrowseTheWeb?: () => BrowseTheWeb | undefined; +} + +/** + * Register a Cucumber `After` hook that captures a Playwright screenshot on failure. + * + * The hook is best-effort: screenshot failures are swallowed so the original + * scenario failure remains the primary signal. + * + * @param options Screenshot directory and optional browser ability provider. + */ +export function registerScreenshotOnFailureHook(options: ScreenshotOnFailureOptions): void { + if (!options.enabled) return; + + After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) { + if (result?.status !== Status.FAILED) { + return; + } + + try { + const browseTheWeb = options.getBrowseTheWeb?.() ?? BrowseTheWeb.current(); + if (!browseTheWeb) { + return; + } + + const reportsDir = resolve(options.reportsDir); + mkdirSync(reportsDir, { recursive: true }); + + const safeName = pickle.name.replaceAll(/[^a-zA-Z0-9-_]/g, '_').slice(0, 80); + const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-'); + const screenshotPath = join(reportsDir, `${safeName}-${timestamp}.png`); + + await browseTheWeb.page.screenshot({ path: screenshotPath, fullPage: true }); + this.attach(readFileSync(screenshotPath), 'image/png'); + } catch { + /* Screenshot capture is best-effort. */ + } + }); +} diff --git a/packages/cellix/serenity-framework/src/cucumber/world.test.ts b/packages/cellix/serenity-framework/src/cucumber/world.test.ts new file mode 100644 index 000000000..ae6f7ff67 --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/world.test.ts @@ -0,0 +1,25 @@ +import type { Cast } from '@serenity-js/core'; +import { describe, expect, it, vi } from 'vitest'; +import { ManagedSerenityWorld } from './world.ts'; + +describe('ManagedSerenityWorld', () => { + it('starts infrastructure, validates state, creates the cast, and resets during cleanup', async () => { + const state = { apiUrl: 'https://api.test/graphql' }; + const infrastructure = { + ensureStarted: vi.fn(), + getState: vi.fn(() => state), + resetScenarioState: vi.fn(), + }; + const validateState = vi.fn(); + const createCast = vi.fn(() => ({ prepare: vi.fn() }) as unknown as Cast); + const world = new ManagedSerenityWorld({ attach: vi.fn(), log: vi.fn(), link: vi.fn(), parameters: {} }, { createCast, infrastructure, validateState }); + + await world.init(); + await world.cleanup(); + + expect(infrastructure.ensureStarted).toHaveBeenCalledOnce(); + expect(validateState).toHaveBeenCalledWith(state); + expect(createCast).toHaveBeenCalledWith(state); + expect(infrastructure.resetScenarioState).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/cellix/serenity-framework/src/cucumber/world.ts b/packages/cellix/serenity-framework/src/cucumber/world.ts new file mode 100644 index 000000000..23e40cacf --- /dev/null +++ b/packages/cellix/serenity-framework/src/cucumber/world.ts @@ -0,0 +1,88 @@ +import { type IWorldOptions, setWorldConstructor, World } from '@cucumber/cucumber'; +import { type Cast, engage } from '@serenity-js/core'; + +/** Infrastructure shape consumed by managed Serenity worlds. */ +export interface ManagedSerenityWorldInfrastructure { + /** Start the suite infrastructure before the scenario uses actors. */ + ensureStarted: () => Promise; + + /** Reset mutable scenario state after each scenario. */ + resetScenarioState?: () => Promise; + + /** Stop suite infrastructure after all scenarios. */ + stopAll?: () => Promise; + + /** Return state needed to construct the scenario cast. */ + getState: () => TState; +} + +/** Options used by {@link ManagedSerenityWorld}. */ +export interface ManagedSerenityWorldOptions { + /** Infrastructure object that owns server and browser lifecycle. */ + infrastructure: ManagedSerenityWorldInfrastructure; + + /** Builds the cast after infrastructure has started. */ + createCast: (state: TState) => Cast; + + /** Optional state assertion run before the cast is engaged. */ + validateState?: (state: TState) => void; +} + +/** + * Base Cucumber world that wires infrastructure state into Serenity/JS. + * + * Extend this class when a suite needs app-specific world methods, or use + * {@link createManagedSerenityWorldClass} when the suite only needs `init` and + * `cleanup`. Consumers supply configuration; the repeated startup, cast + * engagement, and scenario reset pattern stays in the framework. + */ +export class ManagedSerenityWorld extends World { + /** + * @param options Cucumber world options. + * @param config Infrastructure and cast configuration. + */ + constructor( + options: IWorldOptions, + private readonly config: ManagedSerenityWorldOptions, + ) { + super(options); + } + + /** Start infrastructure and engage a Serenity cast for the scenario. */ + async init(): Promise { + await this.config.infrastructure.ensureStarted(); + const state = this.config.infrastructure.getState(); + this.config.validateState?.(state); + engage(this.config.createCast(state)); + } + + /** Reset scenario state through the configured infrastructure. */ + async cleanup(): Promise { + await this.config.infrastructure.resetScenarioState?.(); + } +} + +/** + * Create a Cucumber world class from managed Serenity world configuration. + * + * @param config Infrastructure and cast configuration. + */ +export function createManagedSerenityWorldClass(config: ManagedSerenityWorldOptions): typeof ManagedSerenityWorld { + return class ConfiguredManagedSerenityWorld extends ManagedSerenityWorld { + /** Create the configured world. */ + constructor(options: IWorldOptions) { + super(options, config); + } + }; +} + +/** + * Register a managed Serenity world with Cucumber and return the class. + * + * @param config Infrastructure and cast configuration. + */ +export function registerManagedSerenityWorld(config: ManagedSerenityWorldOptions): typeof ManagedSerenityWorld { + const WorldClass = createManagedSerenityWorldClass(config); + setWorldConstructor(WorldClass); + return WorldClass; +} diff --git a/packages/ocom-verification/verification-shared/src/formatters/agent-formatter.ts b/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts similarity index 77% rename from packages/ocom-verification/verification-shared/src/formatters/agent-formatter.ts rename to packages/cellix/serenity-framework/src/formatters/agent-formatter.ts index b2f8153fa..c06a3b36c 100644 --- a/packages/ocom-verification/verification-shared/src/formatters/agent-formatter.ts +++ b/packages/cellix/serenity-framework/src/formatters/agent-formatter.ts @@ -4,27 +4,37 @@ import type { Envelope, TestCaseFinished, TestRunFinished, TestRunStarted, Times type ParsedTestSteps = ReturnType['testSteps']; const STATUS_ICONS: Record = { - PASSED: 'PASS', + AMBIGUOUS: 'AMBIG', FAILED: 'FAIL', - SKIPPED: 'SKIP', + PASSED: 'PASS', PENDING: 'PEND', + SKIPPED: 'SKIP', UNDEFINED: 'UNDEF', - AMBIGUOUS: 'AMBIG', UNKNOWN: '?', }; -function timestampToMs(ts: Timestamp): number { - return (ts.seconds ?? 0) * 1000 + Math.round((ts.nanos ?? 0) / 1_000_000); +function timestampToMs(timestamp: Timestamp): number { + return (timestamp.seconds ?? 0) * 1000 + Math.round((timestamp.nanos ?? 0) / 1_000_000); } +/** + * Condensed Cucumber formatter intended for agent-readable test output. + * + * The formatter logs failed and warning scenarios with a compact summary, then + * emits aggregate scenario counts and duration at the end of the run. + */ export default class AgentFormatter extends Formatter { - static override readonly documentation = 'Condensed formatter for AI coding agents — minimal, token-efficient output.'; + /** Formatter documentation shown by Cucumber. */ + static override readonly documentation = 'Condensed formatter for AI coding agents: minimal, token-efficient output.'; private testRunStarted: TestRunStarted | undefined; private issueCount = 0; private scenarioCount = 0; private readonly statusCounts: Record = {}; + /** + * @param options Cucumber formatter options. + */ constructor(options: IFormatterOptions) { super(options); options.eventBroadcaster.on('envelope', (envelope: Envelope) => this.parseEnvelope(envelope)); @@ -48,20 +58,19 @@ export default class AgentFormatter extends Formatter { this.statusCounts[statusKey] = (this.statusCounts[statusKey] ?? 0) + 1; const parsed = formatterHelpers.parseTestCaseAttempt({ - testCaseAttempt: attempt, snippetBuilder: this.snippetBuilder, supportCodeLibrary: this.supportCodeLibrary, + testCaseAttempt: attempt, }); const icon = STATUS_ICONS[statusKey] ?? '?'; const { name, sourceLocation } = parsed.testCase; - const loc = sourceLocation ? `${sourceLocation.uri}:${sourceLocation.line}` : ''; - + const location = sourceLocation ? `${sourceLocation.uri}:${sourceLocation.line}` : ''; const isIssue = formatterHelpers.isFailure(attempt.worstTestStepResult, testCaseFinished.willBeRetried) || formatterHelpers.isWarning(attempt.worstTestStepResult, testCaseFinished.willBeRetried); if (isIssue) { this.issueCount++; - this.log(`[${icon}] ${name} (${loc})\n`); + this.log(`[${icon}] ${name} (${location})\n`); this.logFailedSteps(parsed.testSteps); } } @@ -69,7 +78,9 @@ export default class AgentFormatter extends Formatter { private logFailedSteps(testSteps: ParsedTestSteps): void { for (const step of testSteps) { const stepStatus = String(step.result.status); - if (stepStatus === 'PASSED' || stepStatus === 'SKIPPED') continue; + if (stepStatus === 'PASSED' || stepStatus === 'SKIPPED') { + continue; + } const stepIcon = STATUS_ICONS[stepStatus] ?? '?'; const stepText = step.text ?? step.keyword?.trim() ?? '(hook)'; @@ -95,10 +106,7 @@ export default class AgentFormatter extends Formatter { private onTestRunFinished(testRunFinished: TestRunFinished): void { this.log('\n--- (Agent) Results ---\n'); - const parts: string[] = []; - for (const [status, count] of Object.entries(this.statusCounts)) { - parts.push(`${status}: ${count}`); - } + const parts = Object.entries(this.statusCounts).map(([status, count]) => `${status}: ${count}`); this.log(`Scenarios: ${this.scenarioCount} (${parts.join(', ')})\n`); if (this.testRunStarted?.timestamp && testRunFinished.timestamp) { @@ -106,10 +114,6 @@ export default class AgentFormatter extends Formatter { this.log(`Duration: ${ms}ms\n`); } - if (this.issueCount === 0) { - this.log('All scenarios passed.\n'); - } else { - this.log(`Issues: ${this.issueCount}\n`); - } + this.log(this.issueCount === 0 ? 'All scenarios passed.\n' : `Issues: ${this.issueCount}\n`); } } diff --git a/packages/cellix/serenity-framework/src/index.ts b/packages/cellix/serenity-framework/src/index.ts new file mode 100644 index 000000000..c9cd12c6c --- /dev/null +++ b/packages/cellix/serenity-framework/src/index.ts @@ -0,0 +1,11 @@ +export type { GraphQLClientOptions, GraphQLResponse, GraphQLResponseError } from './clients/index.ts'; +export { GraphQLClient } from './clients/index.ts'; +export type { ActorNameResolutionOptions } from './cucumber/actor-name.ts'; +export { ActorName } from './cucumber/actor-name.ts'; +export { GherkinDataTable } from './cucumber/gherkin-data-table.ts'; +export type { ManagedSerenityWorldInfrastructure, ManagedSerenityWorldOptions } from './cucumber/world.ts'; +export { createManagedSerenityWorldClass, ManagedSerenityWorld, registerManagedSerenityWorld } from './cucumber/world.ts'; +export * from './pages/index.ts'; +export * from './serenity/index.ts'; +export * from './servers/index.ts'; +export * from './settings/index.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts new file mode 100644 index 000000000..fe0fb64a8 --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -0,0 +1,244 @@ +import { MongoMemoryTestServer, type MongoMemoryTestServerOptions } from '../servers/mongo-memory-test-server.ts'; +import type { TestServer } from '../servers/test-server.ts'; + +/** Minimal connection shape needed for model cleanup. */ +export interface ManagedMongooseConnection { + /** Registered Mongoose model map. */ + models: Record; + + /** Delete a registered Mongoose model by name. */ + deleteModel(name: string): unknown; +} + +/** Minimal Mongoose service shape managed by API infrastructure. */ +export interface ManagedMongooseService { + /** Start the service. */ + startUp(): Promise | unknown; + + /** Stop the service. */ + shutDown(): Promise | unknown; + + /** Service internals exposing the active connection. */ + service: { + /** Active Mongoose connection. */ + connection: ManagedMongooseConnection; + }; +} + +/** Options used when API infrastructure owns a consumer-provided Mongoose service. */ +export interface ApiInfrastructureMongooseOptions { + /** Create the service from the framework-owned MongoDB connection string. */ + createService: (connectionString: string) => TMongooseService; + + /** Clear registered models after startup. Defaults to true. */ + clearModels?: boolean; +} + +/** Context supplied to the API server factory. */ +export interface ApiServerFactoryContext { + /** Framework-owned MongoDB server, or `undefined` when none is configured. */ + mongoServer: MongoMemoryTestServer | undefined; + + /** Resolve the MongoDB connection string. Throws if no `mongoServer` is configured. */ + getMongoConnectionString: () => string; + + /** Resolve the managed Mongoose service, when configured. */ + getMongooseService: () => TMongooseService; +} + +/** Factory that creates an API server after framework-owned resources are available. */ +export type ApiServerFactory = (context: ApiServerFactoryContext) => TestServer; + +/** Factory used to create the framework-owned MongoDB server. */ +export type ApiMongoServerFactory = (options: MongoMemoryTestServerOptions) => MongoMemoryTestServer; + +/** State exposed by {@link ApiInfrastructure}. */ +export interface ApiInfrastructureState { + /** Running MongoDB server for the suite. */ + mongoServer: MongoMemoryTestServer | undefined; + + /** Running API server for the suite. */ + apiServer: TestServer | undefined; + + /** Running Mongoose service, when configured. */ + mongooseService: TMongooseService | undefined; + + /** API endpoint URL, when the API server has started. */ + apiUrl: string | undefined; +} + +/** Options used by {@link ApiInfrastructure.using}. */ +export interface ApiInfrastructureOptions { + /** MongoDB memory server options. Omit when the suite does not need MongoDB. */ + mongoServer?: MongoMemoryTestServerOptions; + + /** Optional MongoDB server factory. Defaults to `new MongoMemoryTestServer(options)`. */ + createMongoServer?: ApiMongoServerFactory; + + /** Optional Mongoose service managed between MongoDB and the API server. */ + mongoose?: ApiInfrastructureMongooseOptions; + + /** Factory that creates the API server with access to framework-owned resources. */ + createApiServer: ApiServerFactory; +} + +/** + * Lifecycle manager for API acceptance tests. + * + * Use this when a suite needs only the always-present API acceptance base: + * MongoDB plus an API server. Consumers configure the concrete server objects + * with app-specific schema, context, services, seed data, and environment + * values before passing factories to the framework. + * + * @example + * ```ts + * export const infrastructure = ApiInfrastructure.using({ + * mongoServer: { dbName, port, replSetName, seedData }, + * createApiServer: ({ getMongooseService }) => new ApolloGraphQLTestServer({ ... }), + * }); + * ``` + */ +export class ApiInfrastructure { + private readonly mongoServer: MongoMemoryTestServer | undefined; + private apiServer: TestServer | undefined; + private apiUrl: string | undefined; + private mongooseService: TMongooseService | undefined; + private shutdownHandlersRegistered = false; + + private constructor(private readonly options: ApiInfrastructureOptions) { + this.mongoServer = options.mongoServer ? (options.createMongoServer ?? ((mongoOptions) => new MongoMemoryTestServer(mongoOptions)))(options.mongoServer) : undefined; + } + + /** + * Create an API acceptance infrastructure manager. + * + * @param options Required MongoDB options and API server factory. + */ + static using(options: ApiInfrastructureOptions): ApiInfrastructure { + return new ApiInfrastructure(options); + } + + /** Start MongoDB and the API server if they are not already running. */ + async ensureStarted(): Promise { + if (this.apiServer?.isRunning()) { + return; + } + + try { + if (this.mongoServer && !this.mongoServer.isRunning()) { + await this.mongoServer.start(); + } + + await this.ensureMongooseService(); + const apiServer = this.ensureApiServer(); + await apiServer.start(); + this.apiUrl = apiServer.getUrl(); + } catch (error) { + await this.stopAll(); + throw error; + } + } + + /** Reset MongoDB between scenarios without restarting the API server. */ + async resetScenarioState(): Promise { + if (this.mongoServer?.isRunning()) { + await this.mongoServer.resetForScenario(); + } + } + + /** Stop the API server and MongoDB, swallowing shutdown errors from already-failed resources. */ + async stopAll(): Promise { + await this.apiServer?.stop().catch(() => undefined); + this.apiServer = undefined; + + if (this.mongooseService) { + await Promise.resolve(this.mongooseService.shutDown()).catch(() => undefined); + this.mongooseService = undefined; + } + + await this.mongoServer?.stop().catch(() => undefined); + + this.apiUrl = undefined; + } + + /** Return the current infrastructure state. */ + getState(): ApiInfrastructureState { + return { + apiUrl: this.apiUrl, + apiServer: this.apiServer, + mongooseService: this.mongooseService, + mongoServer: this.mongoServer, + }; + } + + /** Register SIGINT and SIGTERM handlers that stop infrastructure before exiting. */ + registerProcessShutdownHandlers(): this { + if (this.shutdownHandlersRegistered) { + return this; + } + + this.shutdownHandlersRegistered = true; + const shutdown = (signal: NodeJS.Signals) => { + void this.stopAll().finally(() => { + process.exit(signal === 'SIGINT' ? 130 : 143); + }); + }; + + process.once('SIGINT', shutdown); + process.once('SIGTERM', shutdown); + return this; + } + + private ensureApiServer(): TestServer { + const { mongoServer } = this; + this.apiServer ??= this.options.createApiServer({ + getMongoConnectionString: mongoServer + ? () => mongoServer.getConnectionString() + : () => { + throw new Error('ApiInfrastructure: no mongoServer configured'); + }, + getMongooseService: () => this.getMongooseService(), + mongoServer, + }); + + return this.apiServer; + } + + private async ensureMongooseService(): Promise { + if (!this.options.mongoose) { + return undefined; + } + + if (!this.mongoServer) { + throw new Error('ApiInfrastructure: mongoose option requires mongoServer to be configured'); + } + + if (!this.mongooseService) { + this.mongooseService = this.options.mongoose.createService(this.mongoServer.getConnectionString()); + await this.mongooseService.startUp(); + if (this.options.mongoose.clearModels ?? true) { + this.clearMongooseModels(this.mongooseService); + } + } + + return this.mongooseService; + } + + private getMongooseService(): TMongooseService { + if (!this.mongooseService) { + throw new Error('ApiInfrastructure Mongoose service is not configured or has not started'); + } + + return this.mongooseService; + } + + private clearMongooseModels(mongooseService: ManagedMongooseService): void { + for (const modelName of Object.keys(mongooseService.service.connection.models)) { + try { + mongooseService.service.connection.deleteModel(modelName); + } catch { + /* already deleted */ + } + } + } +} diff --git a/packages/cellix/serenity-framework/src/infrastructure/api.ts b/packages/cellix/serenity-framework/src/infrastructure/api.ts new file mode 100644 index 000000000..d4cc0bb9b --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/api.ts @@ -0,0 +1,11 @@ +export type { + ApiInfrastructureMongooseOptions, + ApiInfrastructureOptions, + ApiInfrastructureState, + ApiMongoServerFactory, + ApiServerFactory, + ApiServerFactoryContext, + ManagedMongooseConnection, + ManagedMongooseService, +} from './api-infrastructure.ts'; +export { ApiInfrastructure } from './api-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts new file mode 100644 index 000000000..907937c8e --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts @@ -0,0 +1,308 @@ +import { type Browser, type BrowserContext, type BrowserContextOptions, chromium } from 'playwright'; +import { BrowseTheWeb } from '../serenity/browse-the-web.ts'; +import { MongoMemoryTestServer, type MongoMemoryTestServerOptions } from '../servers/mongo-memory-test-server.ts'; +import type { TestServer } from '../servers/test-server.ts'; + +/** UI portal registration used by {@link E2EInfrastructure}. */ +export interface UiPortalRegistration { + /** Stable logical portal name, such as `community` or `staff`. */ + name: TName; + + /** Server that exposes the portal. */ + server: TestServer; +} + +/** State exposed by {@link E2EInfrastructure}. */ +export interface E2EInfrastructureState { + /** Running MongoDB server. */ + mongoServer: MongoMemoryTestServer | undefined; + + /** Running Azurite server. */ + azuriteServer: TestServer | undefined; + + /** Running auth server. */ + authServer: TestServer | undefined; + + /** Running API server. */ + apiServer: TestServer | undefined; + + /** API URL resolved from the API server. */ + apiUrl: string | undefined; + + /** Browser launched for the suite. */ + browser: Browser | undefined; + + /** Browser ability assigned to Serenity actors. */ + browseTheWeb: BrowseTheWeb | undefined; + + /** Base URLs for every registered UI portal. */ + uiPortalBaseUrls: Readonly>; +} + +/** Context supplied to E2E infrastructure callbacks. */ +export interface E2EInfrastructureCallbackContext { + /** Running MongoDB server, or `undefined` when none is configured. */ + mongoServer: MongoMemoryTestServer | undefined; + + /** Running Azurite server. */ + azuriteServer: TestServer; + + /** Running auth server. */ + authServer: TestServer; + + /** Running API server, when phase two has started. */ + apiServer?: TestServer; + + /** Resolve the MongoDB connection string after MongoDB has started. */ + getMongoConnectionString: () => string; +} + +/** Factory that creates an API server once framework-owned MongoDB exists. */ +export type E2EApiServerFactory = (context: E2EInfrastructureCallbackContext) => TestServer; + +/** Factory used to create the framework-owned MongoDB server. */ +export type E2EMongoServerFactory = (options: MongoMemoryTestServerOptions) => MongoMemoryTestServer; + +/** Options used by {@link E2EInfrastructure.using}. */ +export interface E2EInfrastructureOptions { + /** MongoDB memory server options. Omit when the suite does not need MongoDB. */ + mongoServer?: MongoMemoryTestServerOptions; + + /** Optional MongoDB server factory. Defaults to `new MongoMemoryTestServer(options)`. */ + createMongoServer?: E2EMongoServerFactory; + + /** Azurite server required by the suite. */ + azuriteServer: TestServer; + + /** Auth server required by the suite. */ + authServer: TestServer; + + /** Factory that creates the API server with access to framework-owned MongoDB. */ + createApiServer: E2EApiServerFactory; + + /** Suite environment setup, such as starting a local proxy. */ + setupEnvironment?: () => Promise | void; + + /** Suite environment cleanup. */ + cleanupEnvironment?: () => Promise | void; + + /** Launch the browser in headless mode. Defaults to `true`. */ + headless?: boolean; + + /** Browser context options for the authenticated context. */ + browserContextOptions?: BrowserContextOptions | ((state: E2EInfrastructureState) => BrowserContextOptions); +} + +/** + * Lifecycle manager for browser E2E test suites. + * + * The manager requires the base servers that every full-system verification + * suite needs: MongoDB, Azurite, auth, API, and at least one UI portal. Extra + * portals can be chained with {@link addUiPortal}, matching Cellix application + * startup APIs where the invariant pieces are required up front and optional + * pieces are registered fluently. + * + * @example + * ```ts + * export const infrastructure = E2EInfrastructure + * .using({ + * mongoServer: { dbName, port, replSetName, seedData }, + * azuriteServer, + * authServer, + * createApiServer: ({ getMongoConnectionString }) => createApiServer(getMongoConnectionString), + * }) + * .addUiPortal('community', communityPortal) + * .addUiPortal('staff', staffPortal); + * ``` + */ +export class E2EInfrastructure { + private readonly uiPortals = new Map(); + private readonly mongoServer: MongoMemoryTestServer | undefined; + private apiServer: TestServer | undefined; + private environmentReady = false; + private apiUrl: string | undefined; + private browser: Browser | undefined; + private browserContext: BrowserContext | undefined; + private browseTheWeb: BrowseTheWeb | undefined; + private shutdownHandlersRegistered = false; + + private constructor(private readonly options: E2EInfrastructureOptions) { + this.mongoServer = options.mongoServer ? (options.createMongoServer ?? ((mongoOptions) => new MongoMemoryTestServer(mongoOptions)))(options.mongoServer) : undefined; + } + + /** + * Create a browser E2E infrastructure manager. + * + * @param options Required base servers and browser setup. + */ + static using(options: E2EInfrastructureOptions): E2EInfrastructure { + return new E2EInfrastructure(options); + } + + /** + * Register another UI portal server. + * + * @param name Stable logical portal name. + * @param server Portal server. + */ + addUiPortal(name: string, server: TestServer): this { + if (this.uiPortals.has(name)) { + throw new Error(`UI portal '${name}' is already registered`); + } + + this.uiPortals.set(name, server); + return this; + } + + /** Start the environment, base servers, UI portals, browser, and browser ability. */ + async ensureStarted(): Promise { + if (this.uiPortals.size === 0) { + throw new Error('E2EInfrastructure requires at least one UI portal'); + } + + try { + await this.ensureEnvironment(); + await this.startFoundationServers(); + await this.startApplicationServers(); + await this.ensureBrowserAbility(); + } catch (error) { + await this.stopAll(); + throw error; + } + } + + /** Reset mutable scenario state without restarting servers or the browser. */ + async resetScenarioState(): Promise { + if (this.mongoServer?.isRunning()) { + await this.mongoServer.resetForScenario(); + } + } + + /** Stop browser resources, UI portals, base servers, and suite environment. */ + async stopAll(): Promise { + if (this.browseTheWeb) { + await this.browseTheWeb.close().catch(() => undefined); + this.browseTheWeb = undefined; + this.browserContext = undefined; + } else if (this.browserContext) { + await this.browserContext.close().catch(() => undefined); + this.browserContext = undefined; + } + + if (this.browser) { + await this.browser.close().catch(() => undefined); + this.browser = undefined; + } + + await Promise.all([...this.uiPortals.values()].reverse().map((server) => server.stop().catch(() => undefined))); + await this.apiServer?.stop().catch(() => undefined); + await this.options.authServer.stop().catch(() => undefined); + await this.mongoServer?.stop().catch(() => undefined); + await this.options.azuriteServer.stop().catch(() => undefined); + + this.apiUrl = undefined; + this.apiServer = undefined; + + if (this.environmentReady) { + await this.options.cleanupEnvironment?.(); + this.environmentReady = false; + } + } + + /** Return the current infrastructure state. */ + getState(): E2EInfrastructureState { + const uiPortalBaseUrls = Object.fromEntries([...this.uiPortals].map(([name, server]) => [name, server.getUrl()])); + + return { + apiServer: this.apiServer, + apiUrl: this.apiUrl, + authServer: this.options.authServer, + browseTheWeb: this.browseTheWeb, + browser: this.browser, + mongoServer: this.mongoServer, + azuriteServer: this.options.azuriteServer, + uiPortalBaseUrls, + }; + } + + /** Register SIGINT and SIGTERM handlers that stop infrastructure before exiting. */ + registerProcessShutdownHandlers(): this { + if (this.shutdownHandlersRegistered) { + return this; + } + + this.shutdownHandlersRegistered = true; + const shutdown = (signal: NodeJS.Signals) => { + void this.stopAll().finally(() => { + process.exit(signal === 'SIGINT' ? 130 : 143); + }); + }; + + process.once('SIGINT', shutdown); + process.once('SIGTERM', shutdown); + return this; + } + + private async ensureEnvironment(): Promise { + if (this.environmentReady) { + return; + } + + await this.options.setupEnvironment?.(); + this.environmentReady = true; + } + + private async startFoundationServers(): Promise { + await Promise.all([ + this.mongoServer && !this.mongoServer.isRunning() ? this.mongoServer.start() : undefined, + this.options.azuriteServer.isRunning() ? undefined : this.options.azuriteServer.start(), + this.options.authServer.isRunning() ? undefined : this.options.authServer.start(), + ]); + } + + private async startApplicationServers(): Promise { + const apiServer = this.ensureApiServer(); + await Promise.all([apiServer.isRunning() ? undefined : apiServer.start(), ...[...this.uiPortals.values()].map((server) => (server.isRunning() ? undefined : server.start()))]); + + this.apiUrl = apiServer.getUrl(); + } + + private ensureApiServer(): TestServer { + const { mongoServer } = this; + this.apiServer ??= this.options.createApiServer({ + authServer: this.options.authServer, + azuriteServer: this.options.azuriteServer, + getMongoConnectionString: mongoServer + ? () => mongoServer.getConnectionString() + : () => { + throw new Error('E2EInfrastructure: no mongoServer configured'); + }, + mongoServer, + }); + + return this.apiServer; + } + + private async ensureBrowserAbility(): Promise { + if (!this.browser) { + this.browser = await chromium.launch({ headless: this.options.headless ?? true }); + } + + if (this.browseTheWeb) { + return; + } + + const state = this.getState(); + const contextOptions = typeof this.options.browserContextOptions === 'function' ? this.options.browserContextOptions(state) : this.options.browserContextOptions; + this.browserContext = await this.browser.newContext(contextOptions); + const page = await this.browserContext.newPage(); + + try { + this.browseTheWeb = BrowseTheWeb.using(page, this.browserContext); + } catch (error) { + await this.browserContext.close().catch(() => undefined); + this.browserContext = undefined; + throw error; + } + } +} diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e.ts new file mode 100644 index 000000000..3e93f3921 --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e.ts @@ -0,0 +1,9 @@ +export type { + E2EApiServerFactory, + E2EInfrastructureCallbackContext, + E2EInfrastructureOptions, + E2EInfrastructureState, + E2EMongoServerFactory, + UiPortalRegistration, +} from './e2e-infrastructure.ts'; +export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/index.ts b/packages/cellix/serenity-framework/src/infrastructure/index.ts new file mode 100644 index 000000000..5c4ee90c3 --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/index.ts @@ -0,0 +1,4 @@ +export type { ApiInfrastructureOptions, ApiInfrastructureState } from './api.ts'; +export { ApiInfrastructure } from './api.ts'; +export type { E2EInfrastructureCallbackContext, E2EInfrastructureOptions, E2EInfrastructureState, UiPortalRegistration } from './e2e.ts'; +export { E2EInfrastructure } from './e2e.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts new file mode 100644 index 000000000..ad428015e --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts @@ -0,0 +1,158 @@ +import { type Browser, type BrowserContext, chromium, type Page } from 'playwright'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('playwright', () => ({ + chromium: { launch: vi.fn() }, +})); + +import type { MongoMemoryTestServer } from '../servers/mongo-memory-test-server.ts'; +import type { TestServer } from '../servers/test-server.ts'; +import { ApiInfrastructure, E2EInfrastructure } from './index.ts'; + +class FakeServer implements TestServer { + startCalls = 0; + stopCalls = 0; + resetCalls = 0; + private running = false; + + constructor(private readonly url: string) {} + + start(): Promise { + this.startCalls += 1; + this.running = true; + return Promise.resolve(); + } + + stop(): Promise { + this.stopCalls += 1; + this.running = false; + return Promise.resolve(); + } + + isRunning(): boolean { + return this.running; + } + + getUrl(): string { + return this.url; + } + + resetForScenario(): Promise { + this.resetCalls += 1; + return Promise.resolve(); + } +} + +function mongoServer(url = 'mongodb://test'): FakeServer & MongoMemoryTestServer { + return new FakeServer(url) as FakeServer & MongoMemoryTestServer; +} + +function mongoOptions() { + return { + dbName: 'test', + port: 27_017, + replSetName: 'rs0', + }; +} + +function browserStubs() { + const page = { + close: vi.fn(), + isClosed: vi.fn(() => false), + } as unknown as Page; + const context = { + close: vi.fn(), + newPage: vi.fn(async () => page), + } as unknown as BrowserContext; + const browser = { + close: vi.fn(), + newContext: vi.fn(async () => context), + } as unknown as Browser; + + return { browser, context, page }; +} + +describe('ApiInfrastructure', () => { + it('starts MongoDB before the API server and exposes the API URL', async () => { + const mongo = mongoServer(); + const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); + + const infrastructure = ApiInfrastructure.using({ + createApiServer: () => graphQL, + createMongoServer: () => mongo, + mongoServer: mongoOptions(), + }); + + await infrastructure.ensureStarted(); + + expect(infrastructure.getState().apiUrl).toBe('http://127.0.0.1:4000/graphql'); + expect(graphQL.startCalls).toBe(1); + }); + + it('resets MongoDB without restarting the GraphQL server between scenarios', async () => { + const mongo = mongoServer(); + const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); + const infrastructure = ApiInfrastructure.using({ + createApiServer: () => graphQL, + createMongoServer: () => mongo, + mongoServer: mongoOptions(), + }); + + await infrastructure.ensureStarted(); + await infrastructure.resetScenarioState(); + await infrastructure.ensureStarted(); + + expect(mongo.resetCalls).toBe(1); + expect(graphQL.startCalls).toBe(1); + }); +}); + +describe('E2EInfrastructure', () => { + it('requires at least one chained UI portal and exposes all portal URLs', async () => { + const mongo = mongoServer(); + const azurite = new FakeServer('http://127.0.0.1:10000'); + const auth = new FakeServer('https://auth.test'); + const api = new FakeServer('https://api.test/api/graphql'); + const community = new FakeServer('https://community.test'); + const staff = new FakeServer('https://staff.test'); + const { browser } = browserStubs(); + vi.mocked(chromium.launch).mockResolvedValue(browser); + + const infrastructure = E2EInfrastructure.using({ + authServer: auth, + azuriteServer: azurite, + createApiServer: () => api, + createMongoServer: () => mongo, + mongoServer: mongoOptions(), + }) + .addUiPortal('community', community) + .addUiPortal('staff', staff); + + await infrastructure.ensureStarted(); + + expect(infrastructure.getState().uiPortalBaseUrls).toEqual({ + community: 'https://community.test', + staff: 'https://staff.test', + }); + expect(api.startCalls).toBe(1); + expect(staff.startCalls).toBe(1); + }); + + it('creates the browser ability without owning app login behavior', async () => { + const mongo = mongoServer(); + const { browser } = browserStubs(); + vi.mocked(chromium.launch).mockResolvedValue(browser); + + const infrastructure = E2EInfrastructure.using({ + authServer: new FakeServer('https://auth.test'), + azuriteServer: new FakeServer('http://127.0.0.1:10000'), + createApiServer: () => new FakeServer('https://api.test/api/graphql'), + createMongoServer: () => mongo, + mongoServer: mongoOptions(), + }).addUiPortal('community', new FakeServer('https://community.test')); + + await infrastructure.ensureStarted(); + + expect(infrastructure.getState().browseTheWeb).toBeDefined(); + }); +}); diff --git a/packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts b/packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts new file mode 100644 index 000000000..eab193590 --- /dev/null +++ b/packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts @@ -0,0 +1,54 @@ +/** + * ESM loader hooks that intercept CSS, image, and other non-JS imports so + * they resolve to empty modules instead of throwing in Node.js. + * + * Usage: `NODE_OPTIONS='--import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js` + */ + +const ASSET_RE = /\.(css|less|scss|sass|svg|png|jpe?g|gif|webp|woff2?|ttf|eot|ico)$/i; + +/** Minimal loader context needed by the asset hook. */ +export interface AssetLoaderResolveContext { + /** URL of the module importing the asset, supplied by Node's loader API. */ + parentURL?: string; +} + +/** Result returned by Node's ESM resolve hook. */ +export interface AssetLoaderResolveResult { + /** Whether the loader chain should stop at this result. */ + shortCircuit?: boolean; + + /** Resolved module URL. */ + url: string; +} + +/** Next resolver supplied by Node's ESM loader chain. */ +export type NextAssetLoaderResolve = (specifier: string, context: AssetLoaderResolveContext) => Promise; + +/** + * Resolve CSS, image, font, and Ant Design ESM imports for jsdom acceptance tests. + * + * Asset imports resolve to empty JavaScript modules. Ant Design `antd/es/*` + * imports are redirected to `antd/lib/*` when possible because many Node-based + * component tests execute through CommonJS-compatible package output. + */ +export async function resolve(specifier: string, context: AssetLoaderResolveContext, nextResolve: NextAssetLoaderResolve): Promise { + if (ASSET_RE.test(specifier)) { + return { + shortCircuit: true, + url: `data:text/javascript,export default ''`, + }; + } + + // Redirect antd/es/* to antd/lib/* for CJS/ESM compatibility in Node.js + if (specifier.includes('antd/es/')) { + const redirected = specifier.replace('antd/es/', 'antd/lib/'); + try { + return await nextResolve(redirected, context); + } catch { + // fall through to default + } + } + + return nextResolve(specifier, context); +} diff --git a/packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts b/packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts new file mode 100644 index 000000000..6b7d0666d --- /dev/null +++ b/packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: Record; + export default classes; +} diff --git a/packages/cellix/serenity-framework/src/jsdom/css-modules.ts b/packages/cellix/serenity-framework/src/jsdom/css-modules.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/serenity-framework/src/jsdom/css-modules.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom.d.ts b/packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts similarity index 100% rename from packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom.d.ts rename to packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts diff --git a/packages/cellix/serenity-framework/src/jsdom/react-render.ts b/packages/cellix/serenity-framework/src/jsdom/react-render.ts new file mode 100644 index 000000000..fb1065fe2 --- /dev/null +++ b/packages/cellix/serenity-framework/src/jsdom/react-render.ts @@ -0,0 +1,47 @@ +import { type RenderResult, render } from '@testing-library/react'; +import type React from 'react'; + +/** Wraps a rendered React element before it is mounted. */ +export type ReactRenderWrapper = (children: React.ReactElement) => React.ReactElement; + +/** Options used by {@link mountComponent}. */ +export interface ReactMountOptions { + /** Optional wrapper used for providers such as routing, theme, or GraphQL. */ + wrapper?: ReactRenderWrapper; +} + +let rendered: RenderResult | null = null; + +/** + * Mount a React element into the active jsdom document. + * + * Any previously mounted element is unmounted first so component-level + * acceptance tests do not leak state between scenarios. + * + * @param ui React element to mount. + * @param options Optional provider wrapper. + * @returns Testing Library render result for the mounted component. + */ +export function mountComponent(ui: React.ReactElement, options?: ReactMountOptions): RenderResult { + unmountComponent(); + + rendered = render(options?.wrapper ? options.wrapper(ui) : ui); + return rendered; +} + +/** + * Unmount the currently mounted component, when one exists. + */ +export function unmountComponent(): void { + if (rendered) { + rendered.unmount(); + rendered = null; + } +} + +/** + * Return the current Testing Library render result. + */ +export function getRendered(): RenderResult | null { + return rendered; +} diff --git a/packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts b/packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts new file mode 100644 index 000000000..d13d50cd8 --- /dev/null +++ b/packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts @@ -0,0 +1,10 @@ +/** + * Registers the asset-loader ESM hooks so CSS/image imports resolve + * without errors in Node.js. + * + * Use via NODE_OPTIONS: + * `NODE_OPTIONS='--import @cellix/serenity-framework/jsdom/register-asset-loader'`. + */ +import { register } from 'node:module'; + +register(new URL('./asset-loader-hooks.js', import.meta.url).href, import.meta.url); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts b/packages/cellix/serenity-framework/src/jsdom/setup.ts similarity index 100% rename from packages/ocom-verification/acceptance-ui/src/shared/support/ui/jsdom-setup.ts rename to packages/cellix/serenity-framework/src/jsdom/setup.ts diff --git a/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts b/packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts similarity index 57% rename from packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts rename to packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts index 59c5cf3e1..c82c7c412 100644 --- a/packages/ocom-verification/verification-shared/src/pages/adapters/jsdom-adapter.ts +++ b/packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts @@ -1,5 +1,5 @@ import { act, fireEvent } from '@testing-library/react'; -import type { ElementHandle, PageAdapter, PageNavigationWaitUntil, PageUrlMatcher } from '../page-adapter.ts'; +import type { ElementHandle, ElementWaitOptions, PageAdapter, PageNavigationOptions, PageUrlMatcher } from '../page-adapter.ts'; function getGlobalDocument(container: Element): Document { return container.ownerDocument ?? document; @@ -12,8 +12,7 @@ function findLabelControl(container: Element, text: string): Element | null { if (matchingLabel) { const forId = matchingLabel.getAttribute('for'); if (forId) { - const doc = getGlobalDocument(container); - return doc.getElementById(forId); + return getGlobalDocument(container).getElementById(forId); } const wrappedControl = matchingLabel.querySelector('input, textarea, select, [role="textbox"], [role="combobox"], [role="checkbox"]'); @@ -22,21 +21,24 @@ function findLabelControl(container: Element, text: string): Element | null { } } - const ariaMatch = container.querySelector(`[aria-label="${text}"], [aria-label*="${text}"]`); - return ariaMatch; + return container.querySelector(`[aria-label="${text}"], [aria-label*="${text}"]`); } -class JsdomElementHandle implements ElementHandle { - constructor(private readonly el: Element | null) {} +/** + * Element handle backed by a jsdom `Element`. + */ +export class JsdomElementHandle implements ElementHandle { + /** + * @param element Element to adapt, or `null` for a missing selection. + */ + constructor(private readonly element: Element | null) {} fill(value: string): Promise { - if (!this.el) return Promise.resolve(); - - if (!(this.el instanceof HTMLInputElement || this.el instanceof HTMLTextAreaElement)) { + if (!(this.element instanceof HTMLInputElement || this.element instanceof HTMLTextAreaElement)) { return Promise.resolve(); } - const input = this.el; + const input = this.element; const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; act(() => { @@ -51,70 +53,75 @@ class JsdomElementHandle implements ElementHandle { fireEvent.input(input, { target: { value } }); fireEvent.change(input, { target: { value } }); }); + return Promise.resolve(); } click(): Promise { - if (this.el) { - const el = this.el; + if (this.element) { + const element = this.element; act(() => { - fireEvent.click(el); + fireEvent.click(element); }); } return Promise.resolve(); } check(): Promise { - if (this.el instanceof HTMLInputElement) { - const el = this.el; + if (this.element instanceof HTMLInputElement) { + const element = this.element; act(() => { - fireEvent.click(el, { target: { checked: true } }); + fireEvent.click(element, { target: { checked: true } }); }); return Promise.resolve(); } - if (this.el) { - const el = this.el; - act(() => { - fireEvent.click(el); - }); - } - return Promise.resolve(); + return this.click(); } textContent(): Promise { - return Promise.resolve(this.el?.textContent ?? null); + return Promise.resolve(this.element?.textContent ?? null); } getAttribute(name: string): Promise { - return Promise.resolve(this.el?.getAttribute(name) ?? null); + return Promise.resolve(this.element?.getAttribute(name) ?? null); } isVisible(): Promise { - return Promise.resolve(this.el !== null); + return Promise.resolve(this.element !== null); } - waitFor(_options?: { state?: 'visible' | 'hidden' | 'attached' | 'detached'; timeout?: number }): Promise { + waitFor(_options?: ElementWaitOptions): Promise { return Promise.resolve(); } querySelector(selector: string): Promise { - const child = this.el?.querySelector(selector) ?? null; + const child = this.element?.querySelector(selector) ?? null; return Promise.resolve(child ? new JsdomElementHandle(child) : null); } querySelectorAll(selector: string): Promise { - if (!this.el) return Promise.resolve([]); - return Promise.resolve(Array.from(this.el.querySelectorAll(selector)).map((el) => new JsdomElementHandle(el))); + if (!this.element) { + return Promise.resolve([]); + } + return Promise.resolve(Array.from(this.element.querySelectorAll(selector)).map((element) => new JsdomElementHandle(element))); } } +/** + * Page adapter backed by a jsdom container element. + * + * Use this adapter in component-level Cucumber tests that render React into + * jsdom while reusing the same page-object classes used by browser E2E tests. + */ export class JsdomPageAdapter implements PageAdapter { + /** + * @param container Root element that scopes all selections for this page. + */ constructor(private readonly container: Element) {} getByPlaceholder(text: string): ElementHandle { - const el = this.container.querySelector(`[placeholder="${text}"], [placeholder*="${text}"]`); - return new JsdomElementHandle(el); + return new JsdomElementHandle(this.container.querySelector(`[placeholder="${text}"], [placeholder*="${text}"]`)); } getByLabel(text: string): ElementHandle { @@ -123,31 +130,29 @@ export class JsdomPageAdapter implements PageAdapter { getByRole(role: string, options?: { name?: string | RegExp }): ElementHandle { const candidates = Array.from(this.container.querySelectorAll(`[role="${role}"], ${role}`)); - const semanticMap: Record = { button: 'button', - textbox: 'input[type="text"], input:not([type]), textarea', - combobox: 'select, [role="combobox"]', checkbox: 'input[type="checkbox"], [role="checkbox"]', + combobox: 'select, [role="combobox"]', table: 'table', + textbox: 'input[type="text"], input:not([type]), textarea', }; const semanticSelector = semanticMap[role]; + if (semanticSelector) { - const semantic = Array.from(this.container.querySelectorAll(semanticSelector)); - for (const el of semantic) { - if (!candidates.includes(el)) candidates.push(el); + for (const element of Array.from(this.container.querySelectorAll(semanticSelector))) { + if (!candidates.includes(element)) { + candidates.push(element); + } } } const nameFilter = options?.name; if (nameFilter) { - const match = candidates.find((el) => { - const text = el.textContent ?? ''; - const ariaLabel = el.getAttribute('aria-label') ?? ''; - if (nameFilter instanceof RegExp) { - return nameFilter.test(text) || nameFilter.test(ariaLabel); - } - return text.includes(nameFilter) || ariaLabel.includes(nameFilter); + const match = candidates.find((element) => { + const textContent = element.textContent ?? ''; + const ariaLabel = element.getAttribute('aria-label') ?? ''; + return nameFilter instanceof RegExp ? nameFilter.test(textContent) || nameFilter.test(ariaLabel) : textContent.includes(nameFilter) || ariaLabel.includes(nameFilter); }); return new JsdomElementHandle(match ?? null); } @@ -156,19 +161,18 @@ export class JsdomPageAdapter implements PageAdapter { } locator(selector: string): ElementHandle { - const el = this.container.querySelector(selector); - return new JsdomElementHandle(el); + return new JsdomElementHandle(this.container.querySelector(selector)); } locatorAll(selector: string): Promise { - return Promise.resolve(Array.from(this.container.querySelectorAll(selector)).map((el) => new JsdomElementHandle(el))); + return Promise.resolve(Array.from(this.container.querySelectorAll(selector)).map((element) => new JsdomElementHandle(element))); } getByText(text: string | RegExp, options?: { selector?: string }): ElementHandle { const scope = options?.selector ? (this.container.querySelector(options.selector) ?? this.container) : this.container; const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT); let node: Node | null; - // biome-ignore lint/suspicious/noAssignInExpressions: walker pattern + // biome-ignore lint/suspicious/noAssignInExpressions: tree walkers expose the next node through assignment. while ((node = walker.nextNode())) { const content = node.textContent ?? ''; const matches = text instanceof RegExp ? text.test(content) : content.includes(text); @@ -179,22 +183,17 @@ export class JsdomPageAdapter implements PageAdapter { return new JsdomElementHandle(null); } - goto(url: string, _options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise { - if (typeof globalThis !== 'undefined') { - globalThis.history.pushState({}, '', url); - } + goto(url: string, _options?: PageNavigationOptions): Promise { + globalThis.history?.pushState({}, '', url); return Promise.resolve(); } - waitForURL(_url: PageUrlMatcher, _options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise { + waitForURL(_url: PageUrlMatcher, _options?: PageNavigationOptions): Promise { return Promise.resolve(); } url(): string { - if (typeof globalThis !== 'undefined') { - return globalThis.location.href; - } - return 'about:blank'; + return globalThis.location?.href ?? 'about:blank'; } waitForTimeout(_timeout: number): Promise { diff --git a/packages/ocom-verification/verification-shared/src/pages/adapters/playwright-adapter.ts b/packages/cellix/serenity-framework/src/pages/adapters/playwright-adapter.ts similarity index 76% rename from packages/ocom-verification/verification-shared/src/pages/adapters/playwright-adapter.ts rename to packages/cellix/serenity-framework/src/pages/adapters/playwright-adapter.ts index fbb071706..8c4563fa6 100644 --- a/packages/ocom-verification/verification-shared/src/pages/adapters/playwright-adapter.ts +++ b/packages/cellix/serenity-framework/src/pages/adapters/playwright-adapter.ts @@ -1,7 +1,13 @@ import type { Locator as PlaywrightLocator, Page as PlaywrightPage } from 'playwright'; -import type { ElementHandle, PageAdapter, PageNavigationWaitUntil, PageUrlMatcher } from '../page-adapter.ts'; - -class PlaywrightElementHandle implements ElementHandle { +import type { ElementHandle, ElementWaitOptions, PageAdapter, PageNavigationOptions, PageUrlMatcher } from '../page-adapter.ts'; + +/** + * Element handle backed by a Playwright `Locator`. + */ +export class PlaywrightElementHandle implements ElementHandle { + /** + * @param locator Playwright locator to adapt to the framework element contract. + */ constructor(private readonly locator: PlaywrightLocator) {} async fill(value: string): Promise { @@ -28,7 +34,7 @@ class PlaywrightElementHandle implements ElementHandle { return this.locator.isVisible(); } - async waitFor(options?: { state?: 'visible' | 'hidden' | 'attached' | 'detached'; timeout?: number }): Promise { + async waitFor(options?: ElementWaitOptions): Promise { await this.locator.waitFor(options); } @@ -49,7 +55,16 @@ class PlaywrightElementHandle implements ElementHandle { } } +/** + * Page adapter backed by a Playwright `Page`. + * + * Use this adapter at the edge of an E2E test package, then pass it into + * app-specific page objects that depend only on {@link PageAdapter}. + */ export class PlaywrightPageAdapter implements PageAdapter { + /** + * @param page Playwright page used to resolve locators and navigation. + */ constructor(private readonly page: PlaywrightPage) {} getByPlaceholder(text: string): ElementHandle { @@ -84,11 +99,11 @@ export class PlaywrightPageAdapter implements PageAdapter { return new PlaywrightElementHandle(root.getByText(text).first()); } - async goto(url: string, options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise { + async goto(url: string, options?: PageNavigationOptions): Promise { await this.page.goto(url, options); } - async waitForURL(url: PageUrlMatcher, options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise { + async waitForURL(url: PageUrlMatcher, options?: PageNavigationOptions): Promise { await this.page.waitForURL(url as Parameters[0], options); } diff --git a/packages/cellix/serenity-framework/src/pages/index.ts b/packages/cellix/serenity-framework/src/pages/index.ts new file mode 100644 index 000000000..2daf84b4e --- /dev/null +++ b/packages/cellix/serenity-framework/src/pages/index.ts @@ -0,0 +1,12 @@ +export type { + ElementHandle, + ElementWaitOptions, + ElementWaitState, + PageAdapter, + PageAdapterMode, + PageNavigationOptions, + PageNavigationWaitUntil, + PageUrlMatcher, +} from './page-adapter.ts'; +export type { PageObject } from './page-object.ts'; +export { AdapterBackedPageObject } from './page-object.ts'; diff --git a/packages/cellix/serenity-framework/src/pages/page-adapter.ts b/packages/cellix/serenity-framework/src/pages/page-adapter.ts new file mode 100644 index 000000000..0889f07fb --- /dev/null +++ b/packages/cellix/serenity-framework/src/pages/page-adapter.ts @@ -0,0 +1,103 @@ +/** + * Cross-runtime handle for a single element selected by a {@link PageAdapter}. + * + * Implementations may wrap a Playwright locator, a jsdom element, or another + * browser automation primitive. Page objects should use this interface instead + * of depending on a concrete runtime. + */ +export interface ElementHandle { + /** Fill an editable control with the supplied value. */ + fill(value: string): Promise; + + /** Click the element. */ + click(): Promise; + + /** Check a checkbox-like control. */ + check(): Promise; + + /** Read the element text content, or `null` when no element is available. */ + textContent(): Promise; + + /** Read an element attribute, or `null` when the attribute is missing. */ + getAttribute(name: string): Promise; + + /** Return whether the element is currently visible to the adapter runtime. */ + isVisible(): Promise; + + /** Wait for the element to enter a runtime-supported state. */ + waitFor(options?: ElementWaitOptions): Promise; + + /** Find the first child matching a CSS selector. */ + querySelector(selector: string): Promise; + + /** Find all children matching a CSS selector. */ + querySelectorAll(selector: string): Promise; +} + +/** Wait states supported by cross-runtime element handles. */ +export type ElementWaitState = 'visible' | 'hidden' | 'attached' | 'detached'; + +/** Options used when waiting for an element state. */ +export interface ElementWaitOptions { + /** Runtime-specific element state to wait for. */ + state?: ElementWaitState; + + /** Maximum wait time in milliseconds. */ + timeout?: number; +} + +/** Navigation lifecycle values shared with Playwright-compatible adapters. */ +export type PageNavigationWaitUntil = 'load' | 'domcontentloaded' | 'networkidle' | 'commit'; + +/** URL matcher accepted by cross-runtime page adapters. */ +export type PageUrlMatcher = string | RegExp | ((url: URL) => boolean); + +/** Options used when navigating or waiting for a URL. */ +export interface PageNavigationOptions { + /** Maximum wait time in milliseconds. */ + timeout?: number; + + /** Navigation lifecycle state to wait for. */ + waitUntil?: PageNavigationWaitUntil; +} + +/** + * Runtime-neutral page API for app-specific page objects. + * + * Page objects consume this interface so the same page-object class can run in + * fast jsdom acceptance tests and full Playwright E2E tests. + */ +export interface PageAdapter { + /** Select an element by placeholder text. */ + getByPlaceholder(text: string): ElementHandle; + + /** Select a form control by visible or ARIA label text. */ + getByLabel(text: string): ElementHandle; + + /** Select an element by accessible role and optional accessible name. */ + getByRole(role: string, options?: { name?: string | RegExp }): ElementHandle; + + /** Select the first element matching a CSS selector. */ + locator(selector: string): ElementHandle; + + /** Select all elements matching a CSS selector. */ + locatorAll(selector: string): Promise; + + /** Select the first element containing text, optionally scoped by selector. */ + getByText(text: string | RegExp, options?: { selector?: string }): ElementHandle; + + /** Navigate the current page-like runtime to a URL. */ + goto(url: string, options?: PageNavigationOptions): Promise; + + /** Wait for the current URL to match a string, regex, or predicate. */ + waitForURL(url: PageUrlMatcher, options?: PageNavigationOptions): Promise; + + /** Read the current page URL. */ + url(): string; + + /** Wait for a fixed duration. Prefer runtime events when possible. */ + waitForTimeout(timeout: number): Promise; +} + +/** Supported adapter runtime labels. */ +export type PageAdapterMode = 'jsdom' | 'playwright'; diff --git a/packages/cellix/serenity-framework/src/pages/page-object.test.ts b/packages/cellix/serenity-framework/src/pages/page-object.test.ts new file mode 100644 index 000000000..d2a7f2280 --- /dev/null +++ b/packages/cellix/serenity-framework/src/pages/page-object.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { AdapterBackedPageObject, type PageAdapter } from './index.ts'; + +class TestPage extends AdapterBackedPageObject { + get currentUrl(): string { + return this.adapter.url(); + } +} + +describe('AdapterBackedPageObject', () => { + it('keeps page objects bound to their runtime adapter', () => { + const adapter = { + url: () => 'https://example.test', + } as PageAdapter; + + const page = new TestPage(adapter); + + expect(page.adapter).toBe(adapter); + expect(page.currentUrl).toBe('https://example.test'); + }); +}); diff --git a/packages/cellix/serenity-framework/src/pages/page-object.ts b/packages/cellix/serenity-framework/src/pages/page-object.ts new file mode 100644 index 000000000..1b7f70801 --- /dev/null +++ b/packages/cellix/serenity-framework/src/pages/page-object.ts @@ -0,0 +1,38 @@ +import type { PageAdapter } from './page-adapter.ts'; + +/** + * Contract for page objects backed by a {@link PageAdapter}. + * + * The interface intentionally requires only the adapter relationship. Consumer + * packages define domain-specific methods and locators on their own page object + * classes while preserving the common adapter-based pattern. + */ +export interface PageObject { + /** Runtime-neutral adapter used by the page object. */ + readonly adapter: TAdapter; +} + +/** + * Base class for adapter-backed page objects. + * + * Extend this class when a page object should work against multiple runtimes, + * such as jsdom for acceptance UI tests and Playwright for browser E2E tests. + * + * @example + * ```ts + * class LoginPage extends AdapterBackedPageObject { + * async submit(email: string): Promise { + * await this.adapter.getByLabel('Email').fill(email); + * await this.adapter.getByRole('button', { name: /Sign in/i }).click(); + * } + * } + * ``` + */ +export abstract class AdapterBackedPageObject implements PageObject { + /** + * Create a page object backed by a runtime-specific adapter. + * + * @param adapter Adapter that performs DOM or browser operations. + */ + constructor(public readonly adapter: TAdapter) {} +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/abilities/browse-the-web.ts b/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts similarity index 61% rename from packages/ocom-verification/e2e-tests/src/shared/abilities/browse-the-web.ts rename to packages/cellix/serenity-framework/src/serenity/browse-the-web.ts index 6763e0dea..5eb11f69d 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/abilities/browse-the-web.ts +++ b/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts @@ -4,28 +4,54 @@ import type { BrowserContext, Page } from 'playwright'; const actorBrowserMap = new Map(); let fallbackInstance: BrowseTheWeb | undefined; +/** + * Serenity ability that exposes a Playwright page and browser context. + * + * The ability supports a current fallback page for single-browser test suites + * and optional actor registration for multi-actor scenarios. + */ export class BrowseTheWeb extends Ability { + /** Playwright page used by tasks and page adapters. */ readonly page: Page; private readonly context: BrowserContext; private actorName: string | undefined; + /** + * Create and activate a browser ability. + * + * @param page Playwright page assigned to the ability. + * @param context Playwright browser context that owns the page. + */ static using(page: Page, context: BrowserContext): BrowseTheWeb { const ability = new BrowseTheWeb(page, context); fallbackInstance = ability; return ability; } + /** + * Register this ability for a named actor. + * + * @param name Actor name used by Serenity/JS. + */ registerForActor(name: string): this { this.actorName = name; actorBrowserMap.set(name, this); return this; } + /** + * Resolve the browser ability for an actor. + * + * @param actor Serenity actor or ability host. + * @throws Error when no actor-specific or fallback browser ability exists. + */ static withActor(actor: UsesAbilities): BrowseTheWeb { const actorName = 'name' in actor ? (actor as Actor).name : undefined; if (actorName) { const perActor = actorBrowserMap.get(actorName); - if (perActor) return perActor; + if (perActor) { + return perActor; + } } if (!fallbackInstance) { @@ -34,6 +60,9 @@ export class BrowseTheWeb extends Ability { return fallbackInstance; } + /** + * Return the active fallback browser ability, if one has been registered. + */ static current(): BrowseTheWeb | undefined { return fallbackInstance; } @@ -44,10 +73,14 @@ export class BrowseTheWeb extends Ability { this.context = context; } + /** Browser context that owns the page. */ get browserContext(): BrowserContext { return this.context; } + /** + * Close only the current page and detach this ability from the registry. + */ async closePageOnly(): Promise { if (!this.page.isClosed()) { await this.page.close(); @@ -55,6 +88,9 @@ export class BrowseTheWeb extends Ability { this.detach(); } + /** + * Close the page and browser context, then detach this ability. + */ async close(): Promise { if (!this.page.isClosed()) { await this.page.close(); diff --git a/packages/cellix/serenity-framework/src/serenity/browser.ts b/packages/cellix/serenity-framework/src/serenity/browser.ts new file mode 100644 index 000000000..591c56df4 --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/browser.ts @@ -0,0 +1 @@ +export { BrowseTheWeb } from './browse-the-web.ts'; diff --git a/packages/cellix/serenity-framework/src/serenity/cast.ts b/packages/cellix/serenity-framework/src/serenity/cast.ts new file mode 100644 index 000000000..16e694681 --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/cast.ts @@ -0,0 +1,44 @@ +import { type Ability, type Actor, Cast, Notepad, TakeNotes } from '@serenity-js/core'; + +/** Factory that creates a Serenity ability for an actor. */ +export type SerenityAbilityFactory = (actor: Actor) => Ability; + +/** Options used by {@link SerenityCast}. */ +export interface SerenityCastOptions { + /** Ability factories added to every prepared actor. */ + abilities?: SerenityAbilityFactory[]; + + /** Whether each actor receives a Serenity notepad. */ + useNotepad: boolean; +} + +/** + * Generic Serenity cast for Cellix verification suites. + * + * Consumers provide the ability factories their suite needs. The framework + * supplies a single cast implementation so suites do not need local cast + * subclasses for common GraphQL, browser, or notepad-only actor setup. + */ +export class SerenityCast extends Cast { + private readonly abilities: SerenityAbilityFactory[]; + private readonly useNotepad: boolean; + + /** + * @param options Ability factories and notepad behavior. + */ + constructor(options: SerenityCastOptions) { + super(); + this.abilities = options.abilities ?? []; + this.useNotepad = options.useNotepad; + } + + /** + * Prepare an actor with the configured abilities. + * + * @param actor Actor created by Serenity/JS. + */ + prepare(actor: Actor): Actor { + const abilities = this.abilities.map((factory) => factory(actor)); + return this.useNotepad ? actor.whoCan(TakeNotes.using(Notepad.empty()), ...abilities) : actor.whoCan(...abilities); + } +} diff --git a/packages/cellix/serenity-framework/src/serenity/index.ts b/packages/cellix/serenity-framework/src/serenity/index.ts new file mode 100644 index 000000000..51a735837 --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/index.ts @@ -0,0 +1,3 @@ +export type { SerenityAbilityFactory, SerenityCastOptions } from './cast.ts'; +export { SerenityCast } from './cast.ts'; +export { TaskStep } from './task-step.ts'; diff --git a/packages/cellix/serenity-framework/src/serenity/task-step.test.ts b/packages/cellix/serenity-framework/src/serenity/task-step.test.ts new file mode 100644 index 000000000..669d0b392 --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/task-step.test.ts @@ -0,0 +1,16 @@ +import type { PerformsActivities } from '@serenity-js/core'; +import { describe, expect, it } from 'vitest'; +import { TaskStep } from './index.ts'; + +describe('TaskStep', () => { + it('executes the supplied action with the actor', async () => { + const actor = { name: 'Alice' } as unknown as PerformsActivities & { name: string }; + let observedActor: typeof actor | undefined; + + await new TaskStep('#actor does something useful', (currentActor) => { + observedActor = currentActor; + }).performAs(actor); + + expect(observedActor).toBe(actor); + }); +}); diff --git a/packages/cellix/serenity-framework/src/serenity/task-step.ts b/packages/cellix/serenity-framework/src/serenity/task-step.ts new file mode 100644 index 000000000..dbd38f41b --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/task-step.ts @@ -0,0 +1,29 @@ +import { type PerformsActivities, Task } from '@serenity-js/core'; + +/** + * Serenity task backed by an inline async action. + * + * Use `TaskStep` to keep domain step/task code expressive while avoiding small + * helper functions that only bridge Serenity's `Task` contract. + */ +export class TaskStep extends Task { + /** + * @param description Serenity report description for the task. + * @param action Action executed when the actor performs the task. + */ + constructor( + description: string, + private readonly action: (actor: TActor) => Promise | void, + ) { + super(description); + } + + /** + * Execute the configured action for the supplied actor. + * + * @param actor Actor provided by Serenity/JS. + */ + async performAs(actor: PerformsActivities): Promise { + await this.action(actor as TActor); + } +} diff --git a/packages/cellix/serenity-framework/src/servers/api-test-server.ts b/packages/cellix/serenity-framework/src/servers/api-test-server.ts new file mode 100644 index 000000000..09b927560 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/api-test-server.ts @@ -0,0 +1,13 @@ +import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; + +/** Options used by {@link ApiTestServer}. */ +export type ApiTestServerOptions = ProcessTestServerOptions; + +/** + * Process-backed API server for verification suites. + * + * The framework supplies lifecycle behavior only. Consumers provide every + * command, path, URL, readiness marker, environment value, and probe required + * by their application. + */ +export class ApiTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/apollo-graphql-test-server.ts b/packages/cellix/serenity-framework/src/servers/apollo-graphql-test-server.ts new file mode 100644 index 000000000..625b57e5e --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/apollo-graphql-test-server.ts @@ -0,0 +1,94 @@ +import { ApolloServer, type BaseContext } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import type { GraphQLSchema, ValidationRule } from 'graphql'; +import depthLimit from 'graphql-depth-limit'; +import type { TestServer } from './test-server.ts'; + +const MAX_QUERY_DEPTH = 25; + +/** Options used by {@link ApolloGraphQLTestServer}. */ +export interface ApolloGraphQLTestServerOptions { + /** GraphQL schema served by Apollo. */ + schema: GraphQLSchema; + + /** Context factory passed to Apollo's standalone server. */ + context: Parameters>[1]['context']; + + /** Optional GraphQL validation rules. */ + validationRules?: ValidationRule[]; + + /** Whether batched HTTP requests are allowed. Defaults to `true`. */ + allowBatchedHttpRequests?: boolean; + + /** Whether Apollo introspection is enabled. Defaults to `false`. */ + introspection?: boolean; +} + +/** + * Generic in-process Apollo GraphQL server for acceptance tests. + * + * Consumers provide schema, context, validation rules, and app-specific service + * factories from outside the Cellix framework package. + */ +export class ApolloGraphQLTestServer implements TestServer { + private server: ApolloServer | null = null; + private url: string | null = null; + + /** + * @param options Apollo server contract supplied by the consumer. + */ + constructor(private readonly options: ApolloGraphQLTestServerOptions) {} + + /** + * Start the GraphQL server on the specified port, or a random port by default. + * + * @param port TCP port. Use `0` for any available port. + */ + async start(port = 0): Promise { + if (this.server) { + throw new Error('ApolloGraphQLTestServer already started'); + } + + this.server = new ApolloServer({ + allowBatchedHttpRequests: this.options.allowBatchedHttpRequests ?? true, + introspection: this.options.introspection ?? false, + schema: this.options.schema, + validationRules: [depthLimit(MAX_QUERY_DEPTH), ...(this.options.validationRules ?? [])], + }); + + const { url } = await startStandaloneServer(this.server, { + context: this.options.context, + listen: { port }, + }); + + this.url = url; + } + + /** Stop the Apollo server. */ + async stop(): Promise { + if (!this.server) { + return; + } + + await this.server.stop(); + this.server = null; + this.url = null; + } + + /** + * Return the server URL. + * + * @throws Error when the server has not started. + */ + getUrl(): string { + if (!this.url) { + throw new Error('ApolloGraphQLTestServer not started'); + } + return this.url; + } + + /** Return whether the server is active. */ + isRunning(): boolean { + return this.server !== null; + } +} diff --git a/packages/cellix/serenity-framework/src/servers/auth-test-server.ts b/packages/cellix/serenity-framework/src/servers/auth-test-server.ts new file mode 100644 index 000000000..57891aabd --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/auth-test-server.ts @@ -0,0 +1,12 @@ +import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; + +/** Options used by {@link AuthTestServer}. */ +export type AuthTestServerOptions = ProcessTestServerOptions; + +/** + * Process-backed authentication server for verification suites. + * + * Consumers provide the complete descriptor so the framework remains ignorant + * of local auth tools, hostnames, ports, and startup commands. + */ +export class AuthTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts b/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts new file mode 100644 index 000000000..b219c6700 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts @@ -0,0 +1,13 @@ +import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; + +/** Options used by {@link AzuriteTestServer}. */ +export type AzuriteTestServerOptions = ProcessTestServerOptions; + +/** + * Process-backed Azurite server for verification suites. + * + * All app-specific command, port, environment, and readiness details are + * supplied by the consumer so the framework package stays ignorant of local + * workspace conventions. + */ +export class AzuriteTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/index.ts b/packages/cellix/serenity-framework/src/servers/index.ts new file mode 100644 index 000000000..18a418b72 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/index.ts @@ -0,0 +1,18 @@ +export type { ApiTestServerOptions } from './api-test-server.ts'; +export { ApiTestServer } from './api-test-server.ts'; +export type { ApolloGraphQLTestServerOptions } from './apollo-graphql-test-server.ts'; +export { ApolloGraphQLTestServer } from './apollo-graphql-test-server.ts'; +export type { AuthTestServerOptions } from './auth-test-server.ts'; +export { AuthTestServer } from './auth-test-server.ts'; +export type { AzuriteTestServerOptions } from './azurite-test-server.ts'; +export { AzuriteTestServer } from './azurite-test-server.ts'; +export type { MongoMemorySeedContext, MongoMemorySeedDataFunction, MongoMemoryTestServerOptions } from './mongo-memory-test-server.ts'; +export { MongoMemoryTestServer } from './mongo-memory-test-server.ts'; +export { createSpawnEnvironment } from './process-environment.ts'; +export type { ProcessHealthProbe, ProcessTestServerOptions } from './process-test-server.ts'; +export { ProcessTestServer } from './process-test-server.ts'; +export type { SeedDataFunction, TestServer } from './test-server.ts'; +export type { TestServerGroupOptions } from './test-server-group.ts'; +export { TestServerGroup } from './test-server-group.ts'; +export type { UiPortalTestServerOptions } from './ui-portal-test-server.ts'; +export { UiPortalTestServer } from './ui-portal-test-server.ts'; diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts new file mode 100644 index 000000000..f75cae48f --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts @@ -0,0 +1,122 @@ +import { type MongoMemoryReplicaSetConfig, type MongoMemoryReplicaSetDisposer, startMongoMemoryReplicaSet } from '@cellix/server-mongodb-memory-mock-seedwork'; +import { MongoClient } from 'mongodb'; +import type { SeedDataFunction, TestServer } from './test-server.ts'; + +/** Context supplied to Mongo seed functions. */ +export interface MongoMemorySeedContext { + /** MongoDB connection string. */ + connectionString: string; + + /** Database name used by the test server. */ + dbName: string; +} + +/** Seed function used by {@link MongoMemoryTestServer}. */ +export type MongoMemorySeedDataFunction = SeedDataFunction; + +/** Options used by {@link MongoMemoryTestServer}. */ +export interface MongoMemoryTestServerOptions { + /** Database name. */ + dbName: string; + + /** MongoDB port. */ + port: number; + + /** Replica set name. */ + replSetName: string; + + /** MongoDB binary version. */ + binaryVersion?: string; + + /** Optional seed function called after startup and reset. */ + seedData?: MongoMemorySeedDataFunction; +} + +/** + * Reusable in-memory MongoDB replica set for verification tests. + * + * The server is Cellix-only and does not attach application-specific Mongoose + * services. Consumers can seed data through the supplied callback. + */ +export class MongoMemoryTestServer implements TestServer { + private disposer: MongoMemoryReplicaSetDisposer | null = null; + private connectionString = ''; + + /** + * @param options Complete MongoDB memory replica set configuration. + */ + constructor(private readonly options: MongoMemoryTestServerOptions) {} + + /** Start the Mongo memory replica set. */ + async start(): Promise { + const config: MongoMemoryReplicaSetConfig = { + dbName: this.options.dbName, + port: this.options.port, + replSetName: this.options.replSetName, + ...(this.options.binaryVersion && { binaryVersion: this.options.binaryVersion }), + }; + + const { connectionString, disposer } = await startMongoMemoryReplicaSet(config); + this.disposer = disposer; + this.connectionString = connectionString; + await this.seed(); + } + + /** Return the MongoDB connection string. */ + getConnectionString(): string { + if (!this.connectionString) { + throw new Error('MongoMemoryTestServer not started'); + } + return this.connectionString; + } + + /** Alias for {@link getConnectionString}. */ + getUrl(): string { + return this.getConnectionString(); + } + + /** + * Clear all collections and re-run seed data. + * + * @param seedData Optional seed override for this reset. + */ + async resetForScenario(seedData?: MongoMemorySeedDataFunction): Promise { + if (!this.connectionString) { + throw new Error('MongoMemoryTestServer not started'); + } + + await clearDatabase({ connectionString: this.connectionString, dbName: this.options.dbName }); + await this.seed(seedData); + } + + /** Stop the replica set. */ + async stop(): Promise { + if (this.disposer) { + const disposer = this.disposer; + this.disposer = null; + await disposer.stop(); + } + this.connectionString = ''; + } + + /** Return whether the replica set is active. */ + isRunning(): boolean { + return this.disposer !== null; + } + + private async seed(seedData = this.options.seedData): Promise { + await seedData?.({ connectionString: this.connectionString, dbName: this.options.dbName }); + } +} + +async function clearDatabase(context: MongoMemorySeedContext): Promise { + const client = new MongoClient(context.connectionString); + try { + await client.connect(); + const db = client.db(context.dbName); + const collections = await db.listCollections({}, { nameOnly: true }).toArray(); + await Promise.all(collections.map((collection) => db.collection(collection.name).deleteMany({}))); + } finally { + await client.close(); + } +} diff --git a/packages/cellix/serenity-framework/src/servers/process-environment.ts b/packages/cellix/serenity-framework/src/servers/process-environment.ts new file mode 100644 index 000000000..7668a31df --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/process-environment.ts @@ -0,0 +1,11 @@ +/** + * Build a child-process environment while removing inherited Node loader hooks. + * + * This prevents test runner `NODE_OPTIONS` from leaking into app dev servers. + * + * @param overrides Environment variables applied after the current process env. + */ +export function createSpawnEnvironment(overrides: Record = {}): NodeJS.ProcessEnv { + const { NODE_OPTIONS: _ignored, ...baseEnv } = process.env; + return { ...baseEnv, ...overrides }; +} diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.ts new file mode 100644 index 000000000..98954d96b --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.ts @@ -0,0 +1,293 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import { createSpawnEnvironment } from './process-environment.ts'; +import type { TestServer } from './test-server.ts'; + +/** Configuration for health probes used by {@link ProcessTestServer}. */ +export interface ProcessHealthProbe { + /** URL to probe after the ready marker is observed. */ + url: string | (() => string); + + /** Request options supplied to `fetch`. */ + requestInit?: RequestInit | (() => RequestInit); + + /** Predicate that decides whether the probe response is healthy. Defaults to `response.ok`. */ + isHealthy?: (response: Response) => boolean | Promise; +} + +/** Options used by {@link ProcessTestServer}. */ +export interface ProcessTestServerOptions { + /** Human-readable name used in error messages. */ + serverName: string; + + /** Executable to spawn. */ + executable: string | (() => string); + + /** Arguments supplied to the executable. */ + spawnArgs: string[] | (() => string[]); + + /** Working directory for the process. */ + cwd: string; + + /** Marker expected on stdout before health probing begins. */ + readyMarker: string | RegExp; + + /** URL exposed by the server. */ + getUrl: () => string; + + /** Additional process environment values. */ + extraEnv?: Record | (() => Record); + + /** Health probe configuration. Defaults to probing `getUrl()`. Use `false` to trust the ready marker. */ + probe?: ProcessHealthProbe | false; + + /** Maximum startup time in milliseconds. */ + startupTimeoutMs?: number | (() => number); + + /** Maximum graceful shutdown time in milliseconds. */ + shutdownTimeoutMs?: number | (() => number); + + /** Individual health probe timeout in milliseconds. */ + healthProbeTimeoutMs?: number | (() => number); + + /** Delay between health probes in milliseconds. */ + healthProbeIntervalMs?: number | (() => number); + + /** Return true when the server is already reachable before spawning. */ + isAlreadyRunning?: () => Promise; + + /** Treat an early process exit as an existing reusable server. */ + isReusableExit?: (stderrOutput: string) => boolean; +} + +/** + * Configurable child-process test server. + * + * Consumers pass app-specific commands, paths, URLs, and probes through the + * descriptor. The framework owns lifecycle, readiness, probing, and shutdown. + */ +export class ProcessTestServer implements TestServer { + private process: ChildProcess | null = null; + private startedByUs = false; + private readonly useDetachedProcessGroup = process.platform !== 'win32'; + + /** + * @param options Process descriptor and lifecycle settings. + */ + constructor(private readonly options: ProcessTestServerOptions) {} + + /** + * Start the process and wait for readiness. + */ + async start(): Promise { + if (this.process || this.startedByUs) { + return; + } + + if (await this.isAlreadyRunning()) { + return; + } + + const executable = this.value(this.options.executable); + const spawnArgs = this.value(this.options.spawnArgs); + if (!executable || !spawnArgs) { + throw new Error(`${this.options.serverName} requires an executable and spawn arguments`); + } + + this.process = spawn(executable, spawnArgs, { + cwd: this.options.cwd, + env: createSpawnEnvironment(this.value(this.options.extraEnv) ?? {}), + detached: this.useDetachedProcessGroup, + stdio: ['ignore', 'pipe', 'pipe'], + }); + this.startedByUs = true; + + await this.waitForReady(); + } + + /** + * Stop a process started by this instance. + */ + async stop(): Promise { + if (!this.process || !this.startedByUs) { + return; + } + + const childProcess = this.process; + this.process = null; + this.startedByUs = false; + + this.killProcess(childProcess, 'SIGINT'); + + await new Promise((resolve) => { + const timeout = setTimeout(() => { + this.killProcess(childProcess, 'SIGKILL'); + resolve(); + }, this.value(this.options.shutdownTimeoutMs) ?? 10_000); + + childProcess.on('exit', () => { + clearTimeout(timeout); + resolve(); + }); + }); + } + + /** + * Return whether this instance currently owns a running child process. + */ + isRunning(): boolean { + return this.process !== null; + } + + /** + * Return the descriptor URL. + */ + getUrl(): string { + return this.options.getUrl(); + } + + private async isAlreadyRunning(): Promise { + if (this.options.isAlreadyRunning) { + return await this.options.isAlreadyRunning(); + } + return await this.isProbeReadyWithin(this.value(this.options.healthProbeTimeoutMs) ?? 3_000); + } + + private waitForReady(): Promise { + return new Promise((resolve, reject) => { + const childProcess = this.process; + if (!childProcess) { + reject(new Error(`${this.options.serverName} process not started`)); + return; + } + + const startupTimeout = this.value(this.options.startupTimeoutMs) ?? 120_000; + const startupDeadline = Date.now() + startupTimeout; + const timeout = setTimeout(() => { + reject(new Error(`${this.options.serverName} did not start within ${startupTimeout}ms`)); + }, startupTimeout); + + let stderrOutput = ''; + let ready = false; + + const resolveWhenReachable = () => { + if (ready) { + return; + } + ready = true; + + this.waitForProbeReady(startupDeadline, startupTimeout) + .then(() => { + clearTimeout(timeout); + resolve(); + }) + .catch((error: unknown) => { + clearTimeout(timeout); + reject(error); + }); + }; + + childProcess.stdout?.on('data', (data: Buffer) => { + const text = data.toString(); + if (this.matchesReadyMarker(text)) { + resolveWhenReachable(); + } + }); + + childProcess.stderr?.on('data', (data: Buffer) => { + stderrOutput += data.toString(); + }); + + childProcess.on('error', (error: Error) => { + clearTimeout(timeout); + this.process = null; + this.startedByUs = false; + reject(new Error(`${this.options.serverName} failed to start: ${error.message}`)); + }); + + childProcess.on('exit', (code, signal) => { + if (ready) { + return; + } + clearTimeout(timeout); + this.process = null; + this.startedByUs = false; + + if (this.options.isReusableExit?.(stderrOutput)) { + resolve(); + return; + } + + reject(new Error(`${this.options.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); + }); + }); + } + + private async waitForProbeReady(startupDeadline: number, startupTimeout: number): Promise { + const probeInterval = this.value(this.options.healthProbeIntervalMs) ?? 500; + const timeoutError = () => new Error(`${this.options.serverName} did not become healthy within ${startupTimeout}ms`); + + while (true) { + const remainingMs = startupDeadline - Date.now(); + if (remainingMs <= 0) { + throw timeoutError(); + } + + if (await this.isProbeReadyWithin(Math.min(this.value(this.options.healthProbeTimeoutMs) ?? 3_000, remainingMs))) { + return; + } + + const retryDelay = Math.min(probeInterval, startupDeadline - Date.now()); + if (retryDelay <= 0) { + throw timeoutError(); + } + + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + } + + private async isProbeReadyWithin(timeoutMs: number): Promise { + if (this.options.probe === false) { + return true; + } + + let timeout: ReturnType | undefined; + try { + const controller = new AbortController(); + timeout = setTimeout(() => controller.abort(), timeoutMs); + const probe = this.options.probe; + const response = await fetch(this.value(probe?.url) ?? this.getUrl(), { + ...(this.value(probe?.requestInit) ?? {}), + signal: controller.signal, + }); + return probe?.isHealthy ? await probe.isHealthy(response) : response.ok; + } catch { + return false; + } finally { + if (timeout) { + clearTimeout(timeout); + } + } + } + + private matchesReadyMarker(text: string): boolean { + const marker = this.options.readyMarker; + return typeof marker === 'string' ? text.includes(marker) : marker.test(text); + } + + private killProcess(childProcess: ChildProcess, signal: NodeJS.Signals): void { + if (this.useDetachedProcessGroup && childProcess.pid) { + try { + process.kill(-childProcess.pid, signal); + return; + } catch { + /* Fall back to killing the direct child. */ + } + } + + childProcess.kill(signal); + } + + private value(value: T | (() => T) | undefined): T | undefined { + return typeof value === 'function' ? (value as () => T)() : value; + } +} diff --git a/packages/cellix/serenity-framework/src/servers/server-group.test.ts b/packages/cellix/serenity-framework/src/servers/server-group.test.ts new file mode 100644 index 000000000..81fb1659d --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/server-group.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { type TestServer, TestServerGroup } from './index.ts'; + +class RecordingServer implements TestServer { + private readonly name: string; + private readonly calls: string[]; + + constructor(name: string, calls: string[]) { + this.name = name; + this.calls = calls; + } + + start(): Promise { + this.calls.push(`start:${this.name}`); + return Promise.resolve(); + } + + stop(): Promise { + this.calls.push(`stop:${this.name}`); + return Promise.resolve(); + } + + isRunning(): boolean { + return false; + } + + getUrl(): string { + return `https://${this.name}.example.test`; + } +} + +describe('TestServerGroup', () => { + it('starts required servers before variable UI portals and stops in reverse groups', async () => { + const calls: string[] = []; + const group = new TestServerGroup({ + required: [new RecordingServer('api', calls), new RecordingServer('auth', calls)], + uiPortals: [new RecordingServer('community', calls), new RecordingServer('staff', calls)], + }); + + await group.start(); + await group.stop(); + + expect(calls).toEqual(['start:api', 'start:auth', 'start:community', 'start:staff', 'stop:staff', 'stop:community', 'stop:auth', 'stop:api']); + }); +}); diff --git a/packages/cellix/serenity-framework/src/servers/test-server-group.ts b/packages/cellix/serenity-framework/src/servers/test-server-group.ts new file mode 100644 index 000000000..349e4f438 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/test-server-group.ts @@ -0,0 +1,54 @@ +import type { TestServer } from './test-server.ts'; + +/** Options used by {@link TestServerGroup}. */ +export interface TestServerGroupOptions { + /** Servers required for the system under test. */ + required: TestServer[]; + + /** Variable UI portal servers. */ + uiPortals?: TestServer[]; +} + +/** + * Starts and stops required servers plus any number of UI portal servers. + * + * Required servers start before UI portals. Shutdown runs in reverse order. + */ +export class TestServerGroup implements TestServer { + private readonly required: TestServer[]; + private readonly uiPortals: TestServer[]; + + /** + * @param options Required servers and optional UI portals. + */ + constructor(options: TestServerGroupOptions) { + this.required = options.required; + this.uiPortals = options.uiPortals ?? []; + } + + /** Start required servers, then all UI portal servers. */ + async start(): Promise { + await Promise.all(this.required.map((server) => server.start())); + await Promise.all(this.uiPortals.map((server) => server.start())); + } + + /** Stop UI portals, then required servers. */ + async stop(): Promise { + await Promise.all([...this.uiPortals].reverse().map((server) => server.stop().catch(() => undefined))); + await Promise.all([...this.required].reverse().map((server) => server.stop().catch(() => undefined))); + } + + /** Return whether any grouped server reports as running. */ + isRunning(): boolean { + return [...this.required, ...this.uiPortals].some((server) => server.isRunning()); + } + + /** + * Server groups do not expose a single URL. + * + * @throws Error always. + */ + getUrl(): string { + throw new Error('TestServerGroup does not expose a single URL'); + } +} diff --git a/packages/cellix/serenity-framework/src/servers/test-server.ts b/packages/cellix/serenity-framework/src/servers/test-server.ts new file mode 100644 index 000000000..1d337d2d0 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/test-server.ts @@ -0,0 +1,21 @@ +/** + * Common contract for in-process and subprocess test servers. + */ +export interface TestServer { + /** Start the server and resolve when it is ready. */ + start(): Promise; + + /** Stop the server gracefully. */ + stop(): Promise; + + /** Return whether this server instance is currently running. */ + isRunning(): boolean; + + /** Return the URL exposed by the server. */ + getUrl(): string; +} + +/** + * Seed function used by database-oriented test servers. + */ +export type SeedDataFunction = (context: TContext) => Promise | void; diff --git a/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts b/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts new file mode 100644 index 000000000..44221eaee --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts @@ -0,0 +1,13 @@ +import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; + +/** Options used by {@link UiPortalTestServer}. */ +export type UiPortalTestServerOptions = ProcessTestServerOptions; + +/** + * Generic UI portal server for browser E2E suites. + * + * Consumers create one instance per portal and provide every command, path, + * readiness marker, environment value, and URL. The framework intentionally + * does not default to any dev server, executable, or portal naming convention. + */ +export class UiPortalTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/settings/index.ts b/packages/cellix/serenity-framework/src/settings/index.ts new file mode 100644 index 000000000..2fac27525 --- /dev/null +++ b/packages/cellix/serenity-framework/src/settings/index.ts @@ -0,0 +1,2 @@ +export type { DefaultVerificationTimeoutKey, VerificationTimeoutMap, VerificationTimeoutOptions } from './timeout-settings.ts'; +export { defaultVerificationTimeouts, getTimeout, VerificationTimeouts } from './timeout-settings.ts'; diff --git a/packages/cellix/serenity-framework/src/settings/settings.test.ts b/packages/cellix/serenity-framework/src/settings/settings.test.ts new file mode 100644 index 000000000..e3c2afc31 --- /dev/null +++ b/packages/cellix/serenity-framework/src/settings/settings.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { VerificationTimeouts } from './index.ts'; + +describe('VerificationTimeouts', () => { + it('uses positive integer environment overrides', () => { + const timeouts = new VerificationTimeouts({ + defaults: { serverStartup: 100 }, + env: { TIMEOUT_SERVER_STARTUP: '250' }, + }); + + expect(timeouts.get('serverStartup')).toBe(250); + }); + + it('falls back to defaults for invalid overrides', () => { + const timeouts = new VerificationTimeouts({ + defaults: { serverStartup: 100 }, + env: { TIMEOUT_SERVER_STARTUP: 'nope' }, + }); + + expect(timeouts.get('serverStartup')).toBe(100); + }); +}); diff --git a/packages/cellix/serenity-framework/src/settings/timeout-settings.ts b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts new file mode 100644 index 000000000..17abeeaac --- /dev/null +++ b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts @@ -0,0 +1,93 @@ +/** + * Default timeout map used by Cellix verification packages. + */ +export const defaultVerificationTimeouts = { + /** Default Cucumber scenario timeout. */ + scenario: 120_000, + + /** Server startup timeout. */ + serverStartup: 120_000, + + /** Graceful server shutdown timeout. */ + serverShutdown: 10_000, + + /** Health probe timeout. */ + healthProbe: 3_000, + + /** Health probe retry interval. */ + healthProbeInterval: 500, + + /** UI initialization timeout. */ + uiInit: 30_000, + + /** UI cleanup timeout. */ + uiCleanup: 10_000, +} as const; + +/** Keys accepted by the default timeout map. */ +export type DefaultVerificationTimeoutKey = keyof typeof defaultVerificationTimeouts; + +/** Timeout map accepted by {@link VerificationTimeouts}. */ +export type VerificationTimeoutMap = Record; + +/** Options used by {@link VerificationTimeouts}. */ +export interface VerificationTimeoutOptions { + /** Default timeout values. */ + defaults: TTimeouts; + + /** Environment source. Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; +} + +/** + * Reads verification timeouts with optional environment overrides. + * + * Environment variable names are generated from keys: `serverStartup` becomes + * `TIMEOUT_SERVER_STARTUP`. + */ +export class VerificationTimeouts { + private readonly defaults: TTimeouts; + private readonly env: NodeJS.ProcessEnv; + + /** + * @param options Timeout defaults and optional environment source. + */ + constructor(options: VerificationTimeoutOptions) { + this.defaults = options.defaults; + this.env = options.env ?? process.env; + } + + /** + * Get a timeout value, honoring a positive integer environment override. + * + * @param key Timeout key. + */ + get(key: TKey): TTimeouts[TKey] { + const envName = timeoutEnvName(key); + const envOverride = this.env[envName]; + + if (envOverride) { + const parsed = Number(envOverride); + if (Number.isInteger(parsed) && parsed > 0) { + return parsed as TTimeouts[TKey]; + } + } + + return this.defaults[key]; + } +} + +const defaultTimeoutReader = new VerificationTimeouts({ defaults: defaultVerificationTimeouts }); + +/** + * Read a timeout from the default Cellix verification timeout map. + * + * @param key Timeout key. + */ +export function getTimeout(key: DefaultVerificationTimeoutKey): number { + return defaultTimeoutReader.get(key); +} + +function timeoutEnvName(key: string): string { + return `TIMEOUT_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`; +} diff --git a/packages/cellix/serenity-framework/tsconfig.json b/packages/cellix/serenity-framework/tsconfig.json new file mode 100644 index 000000000..39ba28f8b --- /dev/null +++ b/packages/cellix/serenity-framework/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@cellix/config-typescript/node", + "compilerOptions": { + "erasableSyntaxOnly": false, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/cellix/serenity-framework/tsconfig.vitest.json b/packages/cellix/serenity-framework/tsconfig.vitest.json new file mode 100644 index 000000000..4f806efbc --- /dev/null +++ b/packages/cellix/serenity-framework/tsconfig.vitest.json @@ -0,0 +1,3 @@ +{ + "extends": ["./tsconfig.json", "@cellix/config-typescript/vitest"] +} diff --git a/packages/cellix/serenity-framework/turbo.json b/packages/cellix/serenity-framework/turbo.json new file mode 100644 index 000000000..5f90b32dd --- /dev/null +++ b/packages/cellix/serenity-framework/turbo.json @@ -0,0 +1,3 @@ +{ + "extends": ["//"] +} diff --git a/packages/cellix/serenity-framework/vitest.config.ts b/packages/cellix/serenity-framework/vitest.config.ts new file mode 100644 index 000000000..e6ec21dc3 --- /dev/null +++ b/packages/cellix/serenity-framework/vitest.config.ts @@ -0,0 +1,8 @@ +import { nodeConfig } from '@cellix/config-vitest'; +import { mergeConfig } from 'vitest/config'; + +export default mergeConfig(nodeConfig, { + test: { + exclude: ['dist/**', 'node_modules/**'], + }, +}); diff --git a/packages/ocom-verification/acceptance-api/cucumber.js b/packages/ocom-verification/acceptance-api/cucumber.js index d92e97982..b5fb90c59 100644 --- a/packages/ocom-verification/acceptance-api/cucumber.js +++ b/packages/ocom-verification/acceptance-api/cucumber.js @@ -3,7 +3,7 @@ import { isAgent } from 'std-env'; export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], import: ['src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], + format: [...(isAgent ? ['@cellix/serenity-framework/formatters/agent'] : ['progress-bar']), 'json:./reports/cucumber-report-api.json', 'html:./reports/cucumber-report-api.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/acceptance-api/package.json b/packages/ocom-verification/acceptance-api/package.json index 5887447d4..c633606d9 100644 --- a/packages/ocom-verification/acceptance-api/package.json +++ b/packages/ocom-verification/acceptance-api/package.json @@ -18,17 +18,23 @@ "@serenity-js/core": "catalog:", "@serenity-js/cucumber": "catalog:", "@serenity-js/serenity-bdd": "catalog:", + "@cellix/serenity-framework": "workspace:*", + "graphql": "catalog:", + "graphql-depth-limit": "^1.1.0", + "graphql-middleware": "^6.1.35", "std-env": "^4.0.0" }, "devDependencies": { "@cellix/config-typescript": "workspace:*", "@ocom/application-services": "workspace:*", "@ocom/context-spec": "workspace:*", + "@ocom/graphql": "workspace:*", "@ocom/persistence": "workspace:*", "@ocom/service-apollo-server": "workspace:*", "@ocom/service-mongoose": "workspace:*", "@ocom/service-token-validation": "workspace:*", "@ocom-verification/verification-shared": "workspace:*", + "@types/graphql-depth-limit": "^1.1.6", "@types/node": "catalog:", "c8": "^11.0.0", "rimraf": "^6.0.1", diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/notes/header-notes.ts similarity index 100% rename from packages/ocom-verification/acceptance-api/src/contexts/authentication/abilities/header-types.ts rename to packages/ocom-verification/acceptance-api/src/contexts/authentication/notes/header-notes.ts diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts index d204f57f9..f58908f5a 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,6 +1,6 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; -import type { HeaderApiNotes } from '../abilities/header-types.ts'; +import type { HeaderApiNotes } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; let lastActorName = 'Alex'; diff --git a/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts index b0e46f773..04fc28721 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,6 +1,6 @@ -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { TaskStep } from '@cellix/serenity-framework/serenity'; import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; -import type { HeaderApiNotes } from '../abilities/header-types.ts'; +import type { HeaderApiNotes } from '../notes/header-notes.ts'; export const ClickHeaderSignIn = () => Task.where( diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/abilities/community-types.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/notes/community-notes.ts similarity index 100% rename from packages/ocom-verification/acceptance-api/src/contexts/community/abilities/community-types.ts rename to packages/ocom-verification/acceptance-api/src/contexts/community/notes/community-notes.ts diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-name.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-name.ts index c815549eb..c997a105d 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-name.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-name.ts @@ -1,7 +1,7 @@ +import { GraphQLClient } from '@cellix/serenity-framework/clients/graphql'; import { type Actor, type AnswersQuestions, notes, Question, type UsesAbilities } from '@serenity-js/core'; -import { GraphQLClient } from '../../../shared/abilities/graphql-client.ts'; import { GET_COMMUNITY_QUERY } from '../../../shared/graphql/community-operations.ts'; -import type { CommunityNotes } from '../abilities/community-types.ts'; +import type { CommunityNotes } from '../notes/community-notes.ts'; export class CommunityName extends Question> { constructor() { diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-status.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-status.ts index 598bfffd9..0da828fe0 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-status.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/community/questions/community-status.ts @@ -1,5 +1,5 @@ import { type AnswersQuestions, notes, Question, type UsesAbilities } from '@serenity-js/core'; -import type { CommunityNotes } from '../abilities/community-types.ts'; +import type { CommunityNotes } from '../notes/community-notes.ts'; export class CommunityStatus extends Question> { constructor() { diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/step-definitions/create-community.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/step-definitions/create-community.steps.ts index c1f727e2c..a8a7bddce 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/community/step-definitions/create-community.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/community/step-definitions/create-community.steps.ts @@ -1,9 +1,9 @@ +import { ActorName } from '@cellix/serenity-framework/cucumber/actor-name'; +import { GherkinDataTable } from '@cellix/serenity-framework/cucumber/gherkin-data-table'; import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; import { actors } from '@ocom-verification/verification-shared/test-data'; -import { Ensure, equals } from '@serenity-js/assertions'; import { actorCalled, notes } from '@serenity-js/core'; -import { resolveActorName } from '../../../shared/support/domain-test-helpers.ts'; -import type { CommunityDetails, CommunityNotes } from '../abilities/community-types.ts'; +import type { CommunityDetails, CommunityNotes } from '../notes/community-notes.ts'; import { CommunityName } from '../questions/community-name.ts'; import { CommunityStatus } from '../questions/community-status.ts'; import { CreateCommunity } from '../tasks/create-community.ts'; @@ -18,7 +18,7 @@ Given('{word} is an authenticated community owner', (actorName: string) => { When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { lastActorName = actorName; const actor = actorCalled(actorName); - const details = dataTable.rowsHash() as unknown as CommunityDetails; + const details = GherkinDataTable.from(dataTable).rowsHash(); await actor.attemptsTo(CreateCommunity.with(details)); }); @@ -26,7 +26,7 @@ When('{word} creates a community with:', async (actorName: string, dataTable: Da When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { lastActorName = actorName; const actor = actorCalled(actorName); - const details = dataTable.rowsHash() as unknown as CommunityDetails; + const details = GherkinDataTable.from(dataTable).rowsHash(); await actor.attemptsTo(notes().set('lastCommunityId', undefined as unknown as string), notes().set('lastValidationError', undefined as unknown as string)); @@ -40,18 +40,24 @@ When('{word} attempts to create a community with:', async (actorName: string, da Then('the community should be created successfully', async () => { const actor = actorCalled(lastActorName); + const status = await actor.answer(CommunityStatus.of()); - await actor.attemptsTo(Ensure.that(CommunityStatus.of(), equals('SUCCESS'))); + if (status !== 'SUCCESS') { + throw new Error(`Expected community status "SUCCESS" but got "${status}"`); + } }); Then('the community name should be {string}', async (expectedName: string) => { const actor = actorCalled(lastActorName); + const actualName = await actor.answer(CommunityName.displayed()); - await actor.attemptsTo(Ensure.that(CommunityName.displayed(), equals(expectedName))); + if (actualName !== expectedName) { + throw new Error(`Expected community name "${expectedName}" but got "${actualName}"`); + } }); Then('{word} should see a community error for {string}', async (actorName: string, fieldName: string) => { - const resolvedActorName = resolveActorName(actorName, lastActorName); + const resolvedActorName = ActorName.resolve(actorName, { defaultName: lastActorName }); const actor = actorCalled(resolvedActorName); let storedError: string | undefined; diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts index 0f530b08f..118c7fbee 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts @@ -1,7 +1,6 @@ +import { CreateCommunity as CreateCommunityAbility } from '@ocom-verification/verification-shared/abilities'; import { type Actor, notes, Task } from '@serenity-js/core'; -import { GraphQLClient } from '../../../shared/abilities/graphql-client.ts'; -import { COMMUNITY_CREATE_MUTATION, GET_COMMUNITY_QUERY } from '../../../shared/graphql/community-operations.ts'; -import type { CommunityDetails, CommunityNotes } from '../abilities/community-types.ts'; +import type { CommunityDetails, CommunityNotes } from '../notes/community-notes.ts'; export class CreateCommunity extends Task { static withName(name: string) { @@ -17,43 +16,9 @@ export class CreateCommunity extends Task { } async performAs(actor: Actor): Promise { - const graphql = GraphQLClient.as(actor); + const community = await CreateCommunityAbility.as(actor).performAs(actor, this.details); - const response = await graphql.execute(COMMUNITY_CREATE_MUTATION, { - input: { name: this.details.name }, - }); - - const mutationResult = response.data['communityCreate'] as Record; - const status = mutationResult?.['status'] as Record | undefined; - const community = mutationResult?.['community'] as Record | undefined; - - if (status?.['success'] !== true) { - throw new Error(String(status?.['errorMessage'] ?? 'Failed to create community')); - } - - const communityId = String(community?.['id'] ?? ''); - const communityName = String(community?.['name'] ?? ''); - - if (!communityId) { - throw new Error('API communityCreate returned a community without an id'); - } - if (communityName !== this.details.name) { - throw new Error(`API communityCreate returned name "${communityName}", expected "${this.details.name}"`); - } - - const persistedResponse = await graphql.execute(GET_COMMUNITY_QUERY, { - id: communityId, - }); - const persistedData = persistedResponse.data['communityById'] as Record | undefined; - if (!persistedData) { - throw new Error(`Community ${communityId} was not found on re-query; API backend did not persist the community`); - } - const persistedName = String(persistedData['name'] ?? ''); - if (persistedName !== this.details.name) { - throw new Error(`Re-queried community name "${persistedName}" does not match created name "${this.details.name}"`); - } - - await actor.attemptsTo(notes().set('lastCommunityId', communityId), notes().set('lastCommunityName', communityName), notes().set('lastCommunityStatus', 'SUCCESS')); + await actor.attemptsTo(notes().set('lastCommunityId', community.id ?? ''), notes().set('lastCommunityName', community.name), notes().set('lastCommunityStatus', 'SUCCESS')); } override toString = () => `creates a community named "${this.details.name}"`; diff --git a/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts b/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts new file mode 100644 index 000000000..840208333 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts @@ -0,0 +1,47 @@ +import { GraphQLClient } from '@cellix/serenity-framework/clients/graphql'; +import { CreateCommunity } from '@ocom-verification/verification-shared/abilities'; +import { COMMUNITY_CREATE_MUTATION, GET_COMMUNITY_QUERY } from '../graphql/community-operations.ts'; + +export function createCommunityAbility(): CreateCommunity { + return CreateCommunity.using(async (actor, details) => { + const graphql = GraphQLClient.as(actor); + const response = await graphql.execute(COMMUNITY_CREATE_MUTATION, { + input: { name: details.name }, + }); + + const mutationResult = response.data['communityCreate'] as Record; + const status = mutationResult?.['status'] as Record | undefined; + const community = mutationResult?.['community'] as Record | undefined; + + if (status?.['success'] !== true) { + throw new Error(String(status?.['errorMessage'] ?? 'Failed to create community')); + } + + const communityId = String(community?.['id'] ?? ''); + const communityName = String(community?.['name'] ?? ''); + + if (!communityId) { + throw new Error('API communityCreate returned a community without an id'); + } + if (communityName !== details.name) { + throw new Error(`API communityCreate returned name "${communityName}", expected "${details.name}"`); + } + + const persistedResponse = await graphql.execute(GET_COMMUNITY_QUERY, { + id: communityId, + }); + const persistedData = persistedResponse.data['communityById'] as Record | undefined; + if (!persistedData) { + throw new Error(`Community ${communityId} was not found on re-query; API backend did not persist the community`); + } + const persistedName = String(persistedData['name'] ?? ''); + if (persistedName !== details.name) { + throw new Error(`Re-queried community name "${persistedName}" does not match created name "${details.name}"`); + } + + return { + id: communityId, + name: communityName, + }; + }); +} diff --git a/packages/ocom-verification/acceptance-api/src/shared/abilities/graphql-client.ts b/packages/ocom-verification/acceptance-api/src/shared/abilities/graphql-client.ts index 2e6a8beb4..6dd954aea 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/abilities/graphql-client.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/abilities/graphql-client.ts @@ -1,40 +1,10 @@ -import { Ability } from '@serenity-js/core'; - -interface GraphQLResponse { - data: Record; - errors?: Array<{ message: string }>; -} - -export class GraphQLClient extends Ability { - constructor(private readonly apiUrl: string) { - super(); - } - - static at(apiUrl: string): GraphQLClient { - return new GraphQLClient(apiUrl); - } - - async execute(query: string, variables: Record = {}): Promise { - const response = await fetch(this.apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer test-token', - }, - body: JSON.stringify({ query, variables }), - }); - - const result = (await response.json()) as GraphQLResponse; - - if (result.errors && Array.isArray(result.errors)) { - const errorMessage = result.errors.map((err) => err.message ?? 'Unknown error').join('; '); - throw new Error(errorMessage); - } - - if (!response.ok) { - throw new Error(`GraphQL error: ${response.status} ${response.statusText}`); - } - - return result; - } +import { GraphQLClient } from '@cellix/serenity-framework/clients/graphql'; + +export function createGraphQLClientAbility(apiUrl: string): GraphQLClient { + return new GraphQLClient({ + apiUrl, + headers: { + Authorization: 'Bearer test-token', + }, + }); } diff --git a/packages/ocom-verification/acceptance-api/src/shared/abilities/index.ts b/packages/ocom-verification/acceptance-api/src/shared/abilities/index.ts new file mode 100644 index 000000000..2ed5ecf3a --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/shared/abilities/index.ts @@ -0,0 +1,2 @@ +export { createCommunityAbility } from './create-community.ts'; +export { createGraphQLClientAbility } from './graphql-client.ts'; diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/index.ts b/packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts similarity index 100% rename from packages/ocom-verification/acceptance-api/src/shared/support/application-services/index.ts rename to packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts b/packages/ocom-verification/acceptance-api/src/shared/application-services/mock-application-services.ts similarity index 100% rename from packages/ocom-verification/acceptance-api/src/shared/support/application-services/mock-application-services.ts rename to packages/ocom-verification/acceptance-api/src/shared/application-services/mock-application-services.ts diff --git a/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..e25861501 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts @@ -0,0 +1,24 @@ +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import type { IWorld } from '@cucumber/cucumber'; +import { isAgent } from 'std-env'; +import { type CellixApiWorld, stopSharedServers } from '../world.ts'; + +let printedSuiteHeader = false; + +registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + before: async (world) => { + if (!printedSuiteHeader && !isAgent) { + printedSuiteHeader = true; + console.log('\nAPI acceptance tests'); + console.log(' - Community context\n'); + } + + await world.init(); + }, + after: async (world) => { + await world.cleanup(); + }, + afterAll: stopSharedServers, +}); diff --git a/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts new file mode 100644 index 000000000..0531dba81 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts @@ -0,0 +1,39 @@ +import { ApiInfrastructure, type ApiInfrastructureState } from '@cellix/serenity-framework/infrastructure/api'; +import type { ServiceMongoose } from '@ocom/service-mongoose'; +import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; +import { createApiGraphQLServer, createApiMongooseService, resetApiGraphQLServerFactories } from './test-server-factories.ts'; + +const apiDbName = 'owner-community'; + +const infrastructure = ApiInfrastructure.using({ + createApiServer: ({ getMongooseService }) => createApiGraphQLServer(getMongooseService), + mongoServer: { + dbName: apiDbName, + port: 50_000, + replSetName: 'globaldb', + seedData: seedDatabase, + }, + mongoose: { + createService: (connectionString) => createApiMongooseService(connectionString, apiDbName), + }, +}); + +interface InfrastructureState extends ApiInfrastructureState {} + +export function getState(): InfrastructureState { + return infrastructure.getState(); +} + +export async function stopAll(): Promise { + await infrastructure.stopAll(); + resetApiGraphQLServerFactories(); +} + +export async function ensureApiServers(): Promise { + infrastructure.registerProcessShutdownHandlers(); + await infrastructure.ensureStarted(); +} + +export async function resetMongoForScenario(): Promise { + await infrastructure.resetScenarioState(); +} diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/cast.ts b/packages/ocom-verification/acceptance-api/src/shared/support/cast.ts deleted file mode 100644 index 1e92882bb..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/support/cast.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type Actor, type Cast, Notepad, TakeNotes } from '@serenity-js/core'; -import { GraphQLClient } from '../abilities/graphql-client.ts'; - -export class CellixApiCast implements Cast { - constructor(private readonly apiUrl: string) {} - - prepare(actor: Actor): Actor { - return actor.whoCan(TakeNotes.using(Notepad.empty()), GraphQLClient.at(this.apiUrl)); - } -} diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/domain-test-helpers.ts b/packages/ocom-verification/acceptance-api/src/shared/support/domain-test-helpers.ts deleted file mode 100644 index fe02a06ae..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/support/domain-test-helpers.ts +++ /dev/null @@ -1 +0,0 @@ -export { resolveActorName } from '@ocom-verification/verification-shared/helpers'; diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts b/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts deleted file mode 100644 index 9e919f49d..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/support/hooks.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { IWorld } from '@cucumber/cucumber'; -import { After, AfterAll, Before, setDefaultTimeout } from '@cucumber/cucumber'; -import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { isAgent } from 'std-env'; -import { type CellixApiWorld, stopSharedServers } from '../../world.ts'; - -let printedSuiteHeader = false; - -/** Default scenario timeout from centralized configuration */ -setDefaultTimeout(getTimeout('scenario')); - -Before(async function (this: IWorld) { - const world = this as IWorld & CellixApiWorld; - - if (!printedSuiteHeader && !isAgent) { - printedSuiteHeader = true; - console.log('\nAPI acceptance tests'); - console.log(' - Community context\n'); - } - - await world.init(); -}); - -After(async function (this: IWorld) { - const world = this as IWorld & CellixApiWorld; - await world.cleanup(); -}); - -AfterAll(async () => { - await stopSharedServers(); -}); diff --git a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts deleted file mode 100644 index 799cf064b..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/support/shared-infrastructure.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { GraphQLTestServer, MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; -import { createMockApplicationServicesFactory } from './application-services/index.ts'; - -// Shared infrastructure — persists across scenarios within a single test run -let mongoDBServer: MongoDBTestServer | undefined; -let graphQLServer: GraphQLTestServer | undefined; -let apiUrl: string | undefined; - -interface InfrastructureState { - apiUrl: string | undefined; -} - -export function getState(): InfrastructureState { - return { apiUrl }; -} - -export async function stopAll(): Promise { - if (graphQLServer) { - await graphQLServer.stop(); - graphQLServer = undefined; - } - if (mongoDBServer) { - await mongoDBServer.stop(); - mongoDBServer = undefined; - } - apiUrl = undefined; -} - -async function ensureMongoDBServer(): Promise { - if (mongoDBServer) return mongoDBServer; - - mongoDBServer = new MongoDBTestServer(); - await mongoDBServer.start({ attachMongoose: true }); - return mongoDBServer; -} - -export async function ensureApiServers(): Promise { - if (graphQLServer) return; - - const mongo = await ensureMongoDBServer(); - - const mockApplicationServicesFactory = createMockApplicationServicesFactory(mongo.getServiceMongoose()); - graphQLServer = new GraphQLTestServer(mockApplicationServicesFactory); - await graphQLServer.start(); - apiUrl = graphQLServer.getUrl(); -} - -export async function resetMongoForScenario(): Promise { - if (!mongoDBServer) return; - await mongoDBServer.resetForScenario(); -} diff --git a/packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts b/packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts new file mode 100644 index 000000000..02c8ed7d7 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts @@ -0,0 +1,36 @@ +import { ApolloGraphQLTestServer } from '@cellix/serenity-framework/servers'; +import type { ApplicationServices } from '@ocom/application-services'; +import { combinedSchema } from '@ocom/graphql'; +import { ServiceMongoose } from '@ocom/service-mongoose'; +import depthLimit from 'graphql-depth-limit'; +import { applyMiddleware } from 'graphql-middleware'; +import { createMockApplicationServicesFactory } from './application-services/index.ts'; + +let mockApplicationServicesFactory: ReturnType | undefined; + +export function createApiMongooseService(connectionString: string, dbName: string): ServiceMongoose { + return new ServiceMongoose(connectionString, { + autoCreate: true, + autoIndex: true, + dbName, + }); +} + +export function createApiGraphQLServer(getMongooseService: () => ServiceMongoose): ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }> { + return new ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>({ + schema: applyMiddleware(combinedSchema), + validationRules: [depthLimit(10)], + context: async ({ req }) => { + mockApplicationServicesFactory ??= createMockApplicationServicesFactory(getMongooseService()); + const applicationServices = await mockApplicationServicesFactory.forRequest(req.headers.authorization ?? undefined); + if (!applicationServices) { + throw new Error('ApplicationServicesFactory required for test server'); + } + return { applicationServices }; + }, + }); +} + +export function resetApiGraphQLServerFactories(): void { + mockApplicationServicesFactory = undefined; +} diff --git a/packages/ocom-verification/acceptance-api/src/world.ts b/packages/ocom-verification/acceptance-api/src/world.ts index 757aebe3b..3c0adb7a8 100644 --- a/packages/ocom-verification/acceptance-api/src/world.ts +++ b/packages/ocom-verification/acceptance-api/src/world.ts @@ -1,31 +1,31 @@ -import { setWorldConstructor, World } from '@cucumber/cucumber'; -import { engage } from '@serenity-js/core'; -import './shared/support/hooks.ts'; -import { CellixApiCast } from './shared/support/cast.ts'; -import * as infra from './shared/support/shared-infrastructure.ts'; +import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; +import { createCommunityAbility } from './shared/abilities/create-community.ts'; +import { createGraphQLClientAbility } from './shared/abilities/graphql-client.ts'; +import './shared/cucumber-lifecycle-hooks.ts'; +import * as infra from './shared/shared-infrastructure.ts'; export async function stopSharedServers(): Promise { await infra.stopAll(); } -export class CellixApiWorld extends World { - private apiUrl = ''; - - async init(): Promise { - await infra.ensureApiServers(); - await infra.resetMongoForScenario(); - - const { apiUrl } = infra.getState(); - if (apiUrl) { - this.apiUrl = apiUrl; +export const CellixApiWorld = registerManagedSerenityWorld({ + infrastructure: { + ensureStarted: infra.ensureApiServers, + getState: infra.getState, + resetScenarioState: infra.resetMongoForScenario, + stopAll: infra.stopAll, + }, + validateState: (state) => { + if (!state.apiUrl) { + throw new Error('API acceptance infrastructure did not expose an apiUrl'); } + }, + createCast: (state) => + new SerenityCast({ + useNotepad: true, + abilities: [() => createGraphQLClientAbility(state.apiUrl ?? ''), () => createCommunityAbility()], + }), +}); - engage(new CellixApiCast(this.apiUrl)); - } - - async cleanup(): Promise { - // Per-scenario cleanup — extend as needed. - } -} - -setWorldConstructor(CellixApiWorld); +export type CellixApiWorld = InstanceType; diff --git a/packages/ocom-verification/acceptance-ui/cucumber.js b/packages/ocom-verification/acceptance-ui/cucumber.js index e445fafcc..90638142e 100644 --- a/packages/ocom-verification/acceptance-ui/cucumber.js +++ b/packages/ocom-verification/acceptance-ui/cucumber.js @@ -3,7 +3,7 @@ import { isAgent } from 'std-env'; export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], import: ['src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], + format: [...(isAgent ? ['@cellix/serenity-framework/formatters/agent'] : ['progress-bar']), 'json:./reports/cucumber-report-ui.json', 'html:./reports/cucumber-report-ui.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index d9603bcf7..3bac51797 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -5,14 +5,15 @@ "private": true, "type": "module", "scripts": { - "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' cucumber-js", - "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' c8 cucumber-js", - "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import ./src/shared/support/ui/register-asset-loader.ts' c8 cucumber-js" + "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js", + "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' c8 cucumber-js", + "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' c8 cucumber-js" }, "dependencies": { "@apollo/client": "^3.13.9", "@cucumber/cucumber": "catalog:", "@dr.pogodin/react-helmet": "^3.0.4", + "@cellix/serenity-framework": "workspace:*", "@serenity-js/console-reporter": "catalog:", "@serenity-js/core": "catalog:", "@serenity-js/cucumber": "catalog:", @@ -32,7 +33,6 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "c8": "^10.1.3", - "jsdom": "^26.1.0", "tsx": "^4.20.3", "typescript": "catalog:" } diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts similarity index 100% rename from packages/ocom-verification/acceptance-ui/src/contexts/authentication/abilities/header-types.ts rename to packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx index 4cffe5960..7f1348c34 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx @@ -1,12 +1,13 @@ +import { mountComponent } from '@cellix/serenity-framework/jsdom/react-render'; import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; import React from 'react'; import { AuthContext, type AuthContextProps } from 'react-oidc-context'; import { SectionLayout as CommunitySectionLayout } from '../../../../../../ocom/ui-community-route-root/src/section-layout.tsx'; import { SectionLayout as StaffSectionLayout } from '../../../../../../ocom/ui-staff-route-root/src/section-layout.tsx'; -import { mountComponent } from '../../../shared/support/ui/react-render.ts'; +import { wrapOcomComponent } from '../../../shared/ocom-component-wrapper.ts'; import type { CellixUiWorld } from '../../../world.ts'; -import type { HeaderUiNotes } from '../abilities/header-types.ts'; +import type { HeaderUiNotes } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; type Site = 'community' | 'staff'; @@ -77,7 +78,7 @@ When('{word} chooses to sign in', async function (this: CellixUiWorld, _actorNam state.errorCalled = true; }; - const rendered = mountComponent(wrapped); + const rendered = mountComponent(wrapped, { wrapper: wrapOcomComponent() }); this.setHeaderContainer(rendered.container); try { diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts index 5b36010c4..dbf767f8a 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,7 +1,8 @@ -import { HomePage, type UiHomePage } from '@ocom-verification/verification-shared/pages'; -import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { TaskStep } from '@cellix/serenity-framework/serenity'; +import { HomePage } from '@ocom-verification/verification-shared/pages'; import { type Activity, Task } from '@serenity-js/core'; +import type { AcceptanceUiHomePage } from '../../../shared/page-contracts.ts'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -14,7 +15,7 @@ export const ClickHeaderSignIn = (container: HTMLElement) => '#actor clicks the sign-in button on the home page', new TaskStep('#actor clicks the sign-in button', async () => { const adapter = new JsdomPageAdapter(container); - const page: UiHomePage = new HomePage(adapter); + const page: AcceptanceUiHomePage = new HomePage(adapter); await page.clickSignIn(); await flushAsync(); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts similarity index 100% rename from packages/ocom-verification/acceptance-ui/src/contexts/community/abilities/community-types.ts rename to packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-created-flag.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-created-flag.ts index 102d7102f..d232228a1 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-created-flag.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-created-flag.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import type { CommunityUiNotes } from '../notes/community-notes.ts'; export const CommunityCreatedFlag = () => Question.about('whether the community form was submitted', (actor) => actor.answer(notes().get('formSubmitted'))); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts index f1cf03813..61865a446 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import type { CommunityUiNotes } from '../notes/community-notes.ts'; export const CommunityErrorMessage = () => Question.about('the community form error message', (actor) => actor.answer(notes().get('lastValidationError'))); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-name.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-name.ts index e0fbdad59..37dcf8f81 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-name.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-name.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import type { CommunityUiNotes } from '../notes/community-notes.ts'; export const CommunityName = () => Question.about('the submitted community name', (actor) => actor.answer(notes().get('communityName'))); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx index ea8a143d9..ee426c518 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx @@ -1,11 +1,15 @@ +import { ActorName } from '@cellix/serenity-framework/cucumber/actor-name'; +import { GherkinDataTable } from '@cellix/serenity-framework/cucumber/gherkin-data-table'; +import { mountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; -import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; -import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; +import { CommunityPage } from '@ocom-verification/verification-shared/pages'; import { actorCalled, notes } from '@serenity-js/core'; import { CommunityCreate } from '../../../../../../ocom/ui-community-route-accounts/src/components/community-create.tsx'; -import { mountComponent } from '../../../shared/support/ui/react-render.ts'; +import { wrapOcomComponent } from '../../../shared/ocom-component-wrapper.ts'; +import type { AcceptanceUiCommunityPage } from '../../../shared/page-contracts.ts'; import type { CellixUiWorld } from '../../../world.ts'; -import type { CommunityUiNotes } from '../abilities/community-types.ts'; +import type { CommunityUiNotes } from '../notes/community-notes.ts'; import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; import { CommunityErrorMessage } from '../questions/community-error-message.ts'; import { CommunityName } from '../questions/community-name.ts'; @@ -19,7 +23,7 @@ Given('{word} is an authenticated community owner', async function (this: Cellix await actor.attemptsTo(notes().set('formSubmitted', true), notes().set('communityName', values.name ?? ''), notes().set('lastValidationError', '')); }; - const rendered = mountComponent(); + const rendered = mountComponent(, { wrapper: wrapOcomComponent() }); this.setCommunityContainer(rendered.container); await actor.attemptsTo(notes().set('formSubmitted', false), notes().set('communityName', ''), notes().set('lastValidationError', '')); @@ -28,7 +32,7 @@ Given('{word} is an authenticated community owner', async function (this: Cellix When('{word} creates a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { this.setCommunityActorName(actorName); const actor = actorCalled(actorName); - const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; + const { name: communityName = '' } = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); }); @@ -36,7 +40,7 @@ When('{word} creates a community with:', async function (this: CellixUiWorld, ac When('{word} attempts to create a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { this.setCommunityActorName(actorName); const actor = actorCalled(actorName); - const { name: communityName = '' } = dataTable.rowsHash() as { name?: string }; + const { name: communityName = '' } = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); }); @@ -60,11 +64,11 @@ Then('the community name should be {string}', async function (this: CellixUiWorl }); Then('{word} should see a community error for {string}', async function (this: CellixUiWorld, actorName: string, fieldName: string) { - const resolvedName = /^(she|he|they)$/i.test(actorName) ? this.getCommunityActorName() : actorName; + const resolvedName = ActorName.resolve(actorName, { defaultName: this.getCommunityActorName() }); const container = this.getCommunityContainer(); const adapter = new JsdomPageAdapter(container); - const page = new CommunityPage(adapter) as UiCommunityPage; + const page = new CommunityPage(adapter) as AcceptanceUiCommunityPage; let storedError: string | undefined; try { diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index 320723c11..305801dc3 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -1,7 +1,8 @@ -import { CommunityPage, type UiCommunityPage } from '@ocom-verification/verification-shared/pages'; -import { JsdomPageAdapter } from '@ocom-verification/verification-shared/pages/jsdom'; -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { TaskStep } from '@cellix/serenity-framework/serenity'; +import { CommunityPage } from '@ocom-verification/verification-shared/pages'; import { type Activity, Task } from '@serenity-js/core'; +import type { AcceptanceUiCommunityPage } from '../../../shared/page-contracts.ts'; async function flushAsync(): Promise { await new Promise((resolve) => { @@ -17,7 +18,7 @@ export const CreateCommunity = (container: HTMLElement, name: string) => `#actor fills community name "${name}" and submits`, new TaskStep(`#actor submits community name "${name}"`, async () => { const adapter = new JsdomPageAdapter(container); - const page: UiCommunityPage = new CommunityPage(adapter); + const page: AcceptanceUiCommunityPage = new CommunityPage(adapter); await page.fillName(name); await page.clickCreate(); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..0c531114f --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts @@ -0,0 +1,17 @@ +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { unmountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import { After } from '@cucumber/cucumber'; +import type { CellixUiWorld } from '../world.ts'; + +registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + beforeTimeout: getTimeout('uiInit'), + before: async (world) => { + await world.init(); + }, +}); + +After({ timeout: getTimeout('uiCleanup') }, () => { + unmountComponent(); +}); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/ocom-component-wrapper.ts b/packages/ocom-verification/acceptance-ui/src/shared/ocom-component-wrapper.ts new file mode 100644 index 000000000..10b34268b --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/shared/ocom-component-wrapper.ts @@ -0,0 +1,13 @@ +import { MockedProvider, type MockedResponse } from '@apollo/client/testing'; +import { HelmetProvider } from '@dr.pogodin/react-helmet'; +import { App, ConfigProvider } from 'antd'; +import React from 'react'; + +interface OcomComponentWrapperOptions { + mocks?: MockedResponse[]; +} + +export function wrapOcomComponent(options?: OcomComponentWrapperOptions) { + return (children: React.ReactElement): React.ReactElement => + React.createElement(HelmetProvider, null, React.createElement(ConfigProvider, null, React.createElement(App, null, React.createElement(MockedProvider, { mocks: options?.mocks ?? [] }, children)))); +} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/page-contracts.ts b/packages/ocom-verification/acceptance-ui/src/shared/page-contracts.ts new file mode 100644 index 000000000..b7512789f --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/shared/page-contracts.ts @@ -0,0 +1,5 @@ +import type { CommunityPage, HomePage } from '@ocom-verification/verification-shared/pages'; + +export type AcceptanceUiHomePage = Pick; + +export type AcceptanceUiCommunityPage = Pick; diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/cast.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/cast.ts deleted file mode 100644 index 53124613e..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/cast.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Actor, Cast, Notepad, TakeNotes } from '@serenity-js/core'; - -/** - * Cast for acceptance-ui tests — each actor gets a Notepad to share - * state between steps. No server abilities needed because UI tests - * render React components directly in jsdom. - */ -export class CellixUiCast extends Cast { - prepare(actor: Actor): Actor { - return actor.whoCan(TakeNotes.using(Notepad.empty())); - } -} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts deleted file mode 100644 index 212d84a85..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/hooks.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { After, Before, setDefaultTimeout } from '@cucumber/cucumber'; -import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import type { CellixUiWorld } from '../../world.ts'; -import { unmountComponent } from './ui/react-render.ts'; - -/** Default scenario timeout from centralized configuration */ -setDefaultTimeout(getTimeout('scenario')); - -Before({ timeout: getTimeout('uiInit') }, async function (this: CellixUiWorld) { - await this.init(); -}); - -After({ timeout: getTimeout('uiCleanup') }, () => { - unmountComponent(); -}); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/asset-loader-hooks.mjs b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/asset-loader-hooks.mjs deleted file mode 100644 index 472a0c864..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/asset-loader-hooks.mjs +++ /dev/null @@ -1,34 +0,0 @@ -/** - * ESM loader hooks that intercept CSS, image, and other non-JS imports so - * they resolve to empty modules instead of throwing in Node.js. - * - * Usage: `NODE_OPTIONS='--import ./src/shared/support/ui/register-asset-loader.ts' cucumber-js` - */ - -const ASSET_RE = /\.(css|less|scss|sass|svg|png|jpe?g|gif|webp|woff2?|ttf|eot|ico)$/i; - -/** - * @param {string} specifier - * @param {{ parentURL?: string }} context - * @param {Function} nextResolve - */ -export async function resolve(specifier, context, nextResolve) { - if (ASSET_RE.test(specifier)) { - return { - shortCircuit: true, - url: `data:text/javascript,export default ''`, - }; - } - - // Redirect antd/es/* to antd/lib/* for CJS/ESM compatibility in Node.js - if (specifier.includes('antd/es/')) { - const redirected = specifier.replace('antd/es/', 'antd/lib/'); - try { - return await nextResolve(redirected, context); - } catch { - // fall through to default - } - } - - return nextResolve(specifier, context); -} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts deleted file mode 100644 index d48b7eee8..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/react-render.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MockedProvider, type MockedResponse } from '@apollo/client/testing'; -import { HelmetProvider } from '@dr.pogodin/react-helmet'; -import { type RenderResult, render } from '@testing-library/react'; -import { App, ConfigProvider } from 'antd'; -import React from 'react'; - -let rendered: RenderResult | null = null; - -export interface MountOptions { - mocks?: MockedResponse[]; -} - -export function mountComponent(ui: React.ReactElement, options?: MountOptions): RenderResult { - unmountComponent(); - - const wrapped = React.createElement(HelmetProvider, null, React.createElement(ConfigProvider, null, React.createElement(App, null, React.createElement(MockedProvider, { mocks: options?.mocks ?? [] }, ui)))); - - rendered = render(wrapped); - return rendered; -} - -export function unmountComponent(): void { - if (rendered) { - rendered.unmount(); - rendered = null; - } -} - -export function getRendered(): RenderResult | null { - return rendered; -} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/register-asset-loader.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/register-asset-loader.ts deleted file mode 100644 index 917bda93a..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/register-asset-loader.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Registers the asset-loader ESM hooks so CSS/image imports resolve - * without errors in Node.js. - * - * Use via NODE_OPTIONS: `NODE_OPTIONS='--import ./src/shared/support/ui/register-asset-loader.ts'` - * or by adding `--import` to the cucumber-js invocation. - */ -import { register } from 'node:module'; - -register(new URL('./asset-loader-hooks.mjs', import.meta.url).href, import.meta.url); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/setup-jsdom.ts b/packages/ocom-verification/acceptance-ui/src/shared/support/ui/setup-jsdom.ts deleted file mode 100644 index 67f8ee07d..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/support/ui/setup-jsdom.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Side-effect import that initialises the jsdom environment. - * Consumed by cucumber.js --import or NODE_OPTIONS --import. - */ -import './jsdom-setup.ts'; diff --git a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts index 2107af436..cc39388de 100644 --- a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts @@ -3,7 +3,7 @@ * Cucumber imports this file, which then loads all context-specific step definitions. */ -import '../shared/support/ui/setup-jsdom.ts'; -import '../shared/support/hooks.ts'; +import '@cellix/serenity-framework/jsdom/setup'; +import '../shared/cucumber-lifecycle-hooks.ts'; import '../contexts/community/step-definitions/index.ts'; import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/acceptance-ui/src/world.ts b/packages/ocom-verification/acceptance-ui/src/world.ts index 79e0fd932..986ea574d 100644 --- a/packages/ocom-verification/acceptance-ui/src/world.ts +++ b/packages/ocom-verification/acceptance-ui/src/world.ts @@ -1,17 +1,22 @@ -import { setWorldConstructor, World } from '@cucumber/cucumber'; -import { type Cast, serenity } from '@serenity-js/core'; -import { CellixUiCast } from './shared/support/cast.ts'; - -export class CellixUiWorld extends World { - private cast!: Cast; +import { ManagedSerenityWorld, type ManagedSerenityWorldOptions } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; +import { type IWorldOptions, setWorldConstructor } from '@cucumber/cucumber'; + +const uiWorldConfig: ManagedSerenityWorldOptions> = { + infrastructure: { + ensureStarted: () => Promise.resolve(), + getState: () => ({}), + }, + createCast: () => new SerenityCast({ useNotepad: true }), +}; + +export class CellixUiWorld extends ManagedSerenityWorld> { private communityContainer: HTMLElement | null = null; private communityActorName = ''; private headerContainer: HTMLElement | null = null; - init(): Promise { - this.cast = new CellixUiCast(); - serenity.engage(this.cast); - return Promise.resolve(); + constructor(options: IWorldOptions) { + super(options, uiWorldConfig); } setCommunityContainer(container: HTMLElement): void { diff --git a/packages/ocom-verification/acceptance-ui/tsconfig.json b/packages/ocom-verification/acceptance-ui/tsconfig.json index 026eb254a..a1ea4a843 100644 --- a/packages/ocom-verification/acceptance-ui/tsconfig.json +++ b/packages/ocom-verification/acceptance-ui/tsconfig.json @@ -13,5 +13,5 @@ "rootDir": "../..", "outDir": "./dist" }, - "include": ["src/**/*.ts", "src/**/*.tsx", "../../ocom/ui-community-route-root/src/**/*.tsx", "../../ocom/ui-staff-route-root/src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "../../cellix/serenity-framework/src/jsdom/css-module-types.d.ts", "../../ocom/ui-community-route-root/src/**/*.tsx", "../../ocom/ui-staff-route-root/src/**/*.tsx"] } diff --git a/packages/ocom-verification/e2e-tests/cucumber.js b/packages/ocom-verification/e2e-tests/cucumber.js index e548d91e5..38c29160d 100644 --- a/packages/ocom-verification/e2e-tests/cucumber.js +++ b/packages/ocom-verification/e2e-tests/cucumber.js @@ -3,7 +3,7 @@ import { isAgent } from 'std-env'; export default { paths: ['../verification-shared/src/scenarios/**/*.feature'], import: ['src/world.ts', 'src/step-definitions/index.ts'], - format: [...(isAgent ? ['../verification-shared/src/formatters/agent-formatter.ts'] : ['progress-bar']), 'json:./reports/cucumber-report.json', 'html:./reports/cucumber-report.html'], + format: [...(isAgent ? ['@cellix/serenity-framework/formatters/agent'] : ['progress-bar']), 'json:./reports/cucumber-report.json', 'html:./reports/cucumber-report.html'], formatOptions: { snippetInterface: 'async-await', }, diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index d0653152b..7cd34c0a0 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@cucumber/cucumber": "catalog:", + "@cellix/serenity-framework": "workspace:*", "@serenity-js/assertions": "catalog:", "@serenity-js/console-reporter": "catalog:", "@serenity-js/core": "catalog:", diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/notes/header-notes.ts similarity index 100% rename from packages/ocom-verification/e2e-tests/src/contexts/authentication/abilities/header-types.ts rename to packages/ocom-verification/e2e-tests/src/contexts/authentication/notes/header-notes.ts diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index 998ec4dcd..d03d4f5b0 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,9 +1,9 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; import type { BrowserContext, Page } from 'playwright'; -import * as infra from '../../../shared/support/shared-infrastructure.ts'; +import * as infra from '../../../shared/shared-infrastructure.ts'; import type { CellixE2EWorld } from '../../../world.ts'; -import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; interface HeaderE2EState { diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts index bdf690de1..bdbc54a93 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,9 +1,10 @@ -import { type E2EHomePage, HomePage } from '@ocom-verification/verification-shared/pages'; -import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwright'; +import { TaskStep } from '@cellix/serenity-framework/serenity'; +import { HomePage } from '@ocom-verification/verification-shared/pages'; import { type Activity, type Actor, notes, Task } from '@serenity-js/core'; import type { Page } from 'playwright'; -import type { HeaderE2ENotes, HeaderE2ESite } from '../abilities/header-types.ts'; +import type { E2EHomePage } from '../../../shared/page-contracts.ts'; +import type { HeaderE2ENotes, HeaderE2ESite } from '../notes/header-notes.ts'; const portalCredentials: Record = { community: { username: 'test@example.com', password: 'password' }, diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/abilities/community-types.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/notes/community-notes.ts similarity index 100% rename from packages/ocom-verification/e2e-tests/src/contexts/community/abilities/community-types.ts rename to packages/ocom-verification/e2e-tests/src/contexts/community/notes/community-notes.ts diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-created-flag.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-created-flag.ts index f89c53ceb..89b92e21a 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-created-flag.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-created-flag.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +import type { CommunityE2ENotes } from '../notes/community-notes.ts'; export const CommunityCreatedFlag = () => Question.about('whether the community was created', (actor) => actor.answer(notes().get('communityCreated'))); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-error-message.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-error-message.ts index edc11de1a..9d0fa77d8 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-error-message.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-error-message.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +import type { CommunityE2ENotes } from '../notes/community-notes.ts'; export const CommunityErrorMessage = () => Question.about('the community error message', (actor) => actor.answer(notes().get('errorMessage'))); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-name.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-name.ts index 8d4cf0a69..9858c66cb 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-name.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/questions/community-name.ts @@ -1,4 +1,4 @@ import { notes, Question } from '@serenity-js/core'; -import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +import type { CommunityE2ENotes } from '../notes/community-notes.ts'; export const CommunityName = () => Question.about('the name of the created community', (actor) => actor.answer(notes().get('communityName'))); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/step-definitions/create-community.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/step-definitions/create-community.steps.ts index a8d602956..a13835647 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/step-definitions/create-community.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/step-definitions/create-community.steps.ts @@ -1,8 +1,10 @@ +import { ActorName } from '@cellix/serenity-framework/cucumber/actor-name'; +import { GherkinDataTable } from '@cellix/serenity-framework/cucumber/gherkin-data-table'; import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; import { actors } from '@ocom-verification/verification-shared/test-data'; import { actorCalled, notes } from '@serenity-js/core'; -import { OAuth2Login } from '../../../shared/support/oauth2-login.ts'; -import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +import { LogInWithOAuth2 } from '../../../shared/abilities/oauth2-login.ts'; +import type { CommunityE2ENotes } from '../notes/community-notes.ts'; import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; import { CommunityErrorMessage } from '../questions/community-error-message.ts'; import { CommunityName } from '../questions/community-name.ts'; @@ -13,13 +15,13 @@ let lastActorName = actors.CommunityOwner.name; Given('{word} is an authenticated community owner', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - await actor.attemptsTo(OAuth2Login(actors.CommunityOwner.email)); + await actor.attemptsTo(LogInWithOAuth2(actors.CommunityOwner.email)); }); When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { lastActorName = actorName; const actor = actorCalled(actorName); - const details = dataTable.rowsHash(); + const details = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); const name = details['name'] ?? ''; await actor.attemptsTo(CreateCommunity(name)); @@ -28,7 +30,7 @@ When('{word} creates a community with:', async (actorName: string, dataTable: Da When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { lastActorName = actorName; const actor = actorCalled(actorName); - const details = dataTable.rowsHash(); + const details = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); const name = details['name'] ?? ''; try { @@ -58,7 +60,7 @@ Then('the community name should be {string}', async (expectedName: string) => { }); Then('{word} should see a community error for {string}', async (actorName: string, fieldName: string) => { - const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; + const resolvedName = ActorName.resolve(actorName, { defaultName: lastActorName }); const actor = actorCalled(resolvedName); const errorMessage = await actor.answer(CommunityErrorMessage()); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts index 1f67af90e..fa2dbab08 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/community/tasks/create-community.ts @@ -1,10 +1,11 @@ -import { CommunityPage, type E2ECommunityPage } from '@ocom-verification/verification-shared/pages'; -import { PlaywrightPageAdapter } from '@ocom-verification/verification-shared/pages/playwright'; -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; +import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwright'; +import { TaskStep } from '@cellix/serenity-framework/serenity'; +import { BrowseTheWeb } from '@cellix/serenity-framework/serenity/browser'; +import { CommunityPage } from '@ocom-verification/verification-shared/pages'; import { type Activity, type Actor, notes, Task, the } from '@serenity-js/core'; import type { Response } from 'playwright'; -import { BrowseTheWeb } from '../../../shared/abilities/browse-the-web.ts'; -import type { CommunityE2ENotes } from '../abilities/community-types.ts'; +import type { E2ECommunityPage } from '../../../shared/page-contracts.ts'; +import type { CommunityE2ENotes } from '../notes/community-notes.ts'; const createCommunityOperationName = 'AccountsCommunityCreateContainerCommunityCreate'; const communityListOperationName = 'AccountsCommunityListContainerCommunitiesForCurrentEndUser'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/abilities/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/abilities/oauth2-login.ts new file mode 100644 index 000000000..2425aa4f9 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/abilities/oauth2-login.ts @@ -0,0 +1,108 @@ +import { TaskStep } from '@cellix/serenity-framework/serenity'; +import { BrowseTheWeb } from '@cellix/serenity-framework/serenity/browser'; +import { actors } from '@ocom-verification/verification-shared/test-data'; +import { Ability, type Activity, type Actor, Task, the } from '@serenity-js/core'; +import type { Page } from 'playwright'; + +/** Credentials used by the E2E OAuth2 login flow. */ +export interface OAuth2Credentials { + /** Email or username submitted to the mock OAuth2 login form. */ + email: string; + + /** Password submitted to the mock OAuth2 login form. */ + password: string; +} + +/** Options that configure the E2E OAuth2 login ability. */ +export interface OAuth2LoginOptions { + /** Protected route used to trigger the OIDC redirect flow. */ + protectedPath: string; +} + +/** + * URL predicate that resolves once the OIDC redirect chain has settled — + * i.e. we are no longer on the mock-auth hostname or the /auth-redirect + * callback path. + */ +const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'); + +/** + * Authenticates the browser session via the OIDC auto-redirect flow. + * + * The app uses RequireAuth + react-oidc-context. When an unauthenticated + * user hits a protected route, RequireAuth calls `signinRedirect()` which + * navigates to the mock OAuth2 server's `/authorize` endpoint. The mock + * server redirects to `/login` (since userStore is configured). This + * function fills in the test user credentials and submits the form. + */ +export async function performOAuth2Login(page: Page, credentials: OAuth2Credentials, protectedPath: string): Promise { + // Navigate to a protected route to trigger the OIDC signinRedirect flow. + try { + await page.goto(protectedPath, { + waitUntil: 'networkidle', + timeout: 60_000, + }); + } catch { + // Navigation may be interrupted by OIDC redirect — this is expected + } + + // Wait for redirects to settle on either the login page or the app + await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); + + // If the mock OAuth2 login form is shown, fill credentials and submit. + // CommunityOwner is defined in mock-oidc.users.json with password "password". + if (page.url().includes('/login')) { + await page.fill('input[name="username"]', credentials.email); + await page.fill('input[name="password"]', credentials.password); + await page.click('button[type="submit"]'); + } + + // Wait for the redirect chain to settle on an authenticated page + await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); + await page.waitForLoadState('networkidle'); +} + +/** Serenity ability that authenticates an E2E actor through the OCOM OAuth2 flow. */ +export class OAuth2Login extends Ability { + /** + * @param options Route and flow options for the OAuth2 login ability. + */ + constructor(private readonly options: OAuth2LoginOptions) { + super(); + } + + /** + * Create an OAuth2 login ability for the supplied protected route. + * + * @param protectedPath Protected route used to trigger the OIDC redirect flow. + */ + static throughProtectedRoute(protectedPath: string): OAuth2Login { + return new OAuth2Login({ protectedPath }); + } + + /** + * Authenticate the actor's current browser page. + * + * @param actor Actor that has the `BrowseTheWeb` ability. + * @param credentials Credentials submitted to the mock OAuth2 form. + */ + async authenticate(actor: Actor, credentials: OAuth2Credentials): Promise { + const { page } = BrowseTheWeb.withActor(actor); + await performOAuth2Login(page, credentials, this.options.protectedPath); + } +} + +/** + * Screenplay Task — confirms the actor is authenticated. + * + * The browser context is pre-authenticated by {@link performOAuth2Login} + * during server setup. This task navigates to a protected route and + * verifies the page loads without being kicked to the auth provider. + */ +export const LogInWithOAuth2 = (email = actors.CommunityOwner.email, password = 'password') => + Task.where( + the`#actor logs in via OAuth2`, + new TaskStep('#actor confirms the OAuth2 session is active', async (actor) => { + await OAuth2Login.as(actor as Actor).authenticate(actor as Actor, { email, password }); + }) as Activity, + ); diff --git a/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..89b7424b3 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts @@ -0,0 +1,24 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { registerScreenshotOnFailureHook } from '@cellix/serenity-framework/cucumber/screenshot'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import type { IWorld } from '@cucumber/cucumber'; +import { type CellixE2EWorld, stopSharedServers } from '../world.ts'; + +const currentDir = fileURLToPath(new URL('.', import.meta.url)); + +registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + before: async (world) => { + await world.init(); + }, + after: async (world) => { + await world.cleanup(); + }, + afterAll: stopSharedServers, +}); + +registerScreenshotOnFailureHook({ + reportsDir: path.resolve(currentDir, '..', '..', '..', 'reports', 'screenshots'), +}); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts similarity index 86% rename from packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts rename to packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts index d8ea7b022..9a5e789f7 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/app-paths.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts @@ -2,7 +2,7 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const currentDir = dirname(fileURLToPath(import.meta.url)); -const workspaceRoot = resolve(currentDir, '../../../../../../..'); +const workspaceRoot = resolve(currentDir, '../../../../../..'); export const appPaths = { apiDir: resolve(workspaceRoot, 'apps/api'), diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/dev-script.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts similarity index 100% rename from packages/ocom-verification/e2e-tests/src/shared/support/servers/dev-script.ts rename to packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/resolve-portless.ts similarity index 89% rename from packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts rename to packages/ocom-verification/e2e-tests/src/shared/environment/resolve-portless.ts index 42c7f7827..9c5f978f9 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/resolve-portless.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/resolve-portless.ts @@ -3,7 +3,7 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const currentDir = dirname(fileURLToPath(import.meta.url)); -const workspaceRoot = resolve(currentDir, '../../../../../../..'); +const workspaceRoot = resolve(currentDir, '../../../../../..'); let resolvedPath: string | undefined; diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts new file mode 100644 index 000000000..cfb488e4c --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts @@ -0,0 +1,108 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getPortlessPath } from './resolve-portless.ts'; + +let proxyInitialized = false; + +loadE2EEnvDefaults(); + +export type OcomPortlessHostKey = 'api' | 'mockAuth' | 'uiCommunity' | 'uiStaff'; + +export function getHostnames(): Record { + const hostnames = resolvePortlessHostnames({ + keys: { + api: 'VITE_COMMON_API_ENDPOINT', + mockAuth: 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', + uiCommunity: 'VITE_APP_UI_COMMUNITY_BASE_URL', + uiStaff: 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI', + }, + }); + + return { + ...hostnames, + docs: `docs.${hostnames.uiCommunity}`, + }; +} + +const hostnames = getHostnames(); + +export const mockOidcAudience = 'mock-client'; +export const mockOidcIssuer = buildUrl(hostnames.mockAuth, '/community'); +export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; +export const mockStaffOidcIssuer = buildUrl(hostnames.mockAuth, '/staff'); + +/** + * Ensure the portless proxy is running for the PR's worktree-scoped hostnames. + */ +export function initTestEnvironment() { + if (proxyInitialized) return; + + execFileSync(getPortlessPath(), ['prune'], { + timeout: 10_000, + stdio: 'pipe', + }); + execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { + timeout: 15_000, + stdio: 'pipe', + }); + + proxyInitialized = true; +} + +export function buildUrl(hostname: string, path = ''): string { + return `https://${hostname}:1355${path}`; +} + +export function cleanupTestEnvironment(): void { + proxyInitialized = false; +} + +function loadE2EEnvDefaults(): void { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const workspaceRoot = resolve(currentDir, '../../../../../..'); + for (const filePath of [resolve(workspaceRoot, 'apps/ui-community/.env.e2e'), resolve(workspaceRoot, 'apps/ui-staff/.env.e2e')]) { + if (!existsSync(filePath)) continue; + for (const line of readFileSync(filePath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx === -1) continue; + const key = trimmed.slice(0, idx); + process.env[key] ??= trimmed.slice(idx + 1); + } + } +} + +interface ResolvePortlessHostnamesOptions { + keys: Record; + env?: NodeJS.ProcessEnv; + worktreeName?: string; +} + +function resolvePortlessHostnames(options: ResolvePortlessHostnamesOptions): Record { + const env = options.env ?? process.env; + const worktreeName = options.worktreeName ?? env['WORKTREE_NAME'] ?? ''; + const hostnames = {} as Record; + + for (const [logicalName, envName] of Object.entries(options.keys) as Array<[TKey, string]>) { + hostnames[logicalName] = applyWorktreeSuffix(requireHostname(envName, env), worktreeName); + } + + return hostnames; +} + +function applyWorktreeSuffix(hostname: string, worktreeName: string): string { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} + +function requireHostname(key: string, env: NodeJS.ProcessEnv): string { + const url = env[key] ?? ''; + try { + return new URL(url).hostname; + } catch { + throw new Error(`e2e test environment: required env var ${key} is missing or invalid`); + } +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts similarity index 100% rename from packages/ocom-verification/e2e-tests/src/shared/support/servers/worktree-ports.ts rename to packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts diff --git a/packages/ocom-verification/e2e-tests/src/shared/page-contracts.ts b/packages/ocom-verification/e2e-tests/src/shared/page-contracts.ts new file mode 100644 index 000000000..677ab56e7 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/page-contracts.ts @@ -0,0 +1,5 @@ +import type { CommunityPage, HomePage } from '@ocom-verification/verification-shared/pages'; + +export type E2EHomePage = Pick; + +export type E2ECommunityPage = Pick; diff --git a/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts new file mode 100644 index 000000000..318008c19 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts @@ -0,0 +1,60 @@ +import { E2EInfrastructure, type E2EInfrastructureState } from '@cellix/serenity-framework/infrastructure/e2e'; +import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; +import { getMongoPort } from './environment/worktree-ports.ts'; +import { cleanupTestEnvironment, createCommunityUiPortalServer, createStaffUiPortalServer, createTestApiServer, createTestAzuriteServer, createTestOAuth2Server, initTestEnvironment } from './test-server-factories.ts'; + +const apiDbName = 'owner-community'; + +const infrastructure = E2EInfrastructure.using({ + authServer: createTestOAuth2Server(), + azuriteServer: createTestAzuriteServer(), + browserContextOptions: (state) => { + const communityBaseUrl = state.uiPortalBaseUrls['community']; + if (!communityBaseUrl) { + throw new Error('Community UI portal URL was not initialized'); + } + + return { + baseURL: communityBaseUrl, + ignoreHTTPSErrors: true, + }; + }, + cleanupEnvironment: cleanupTestEnvironment, + createApiServer: ({ getMongoConnectionString }) => createTestApiServer(getMongoConnectionString), + mongoServer: { + dbName: apiDbName, + port: getMongoPort(), + replSetName: 'globaldb', + seedData: seedDatabase, + }, + setupEnvironment: initTestEnvironment, +}) + .addUiPortal('community', createCommunityUiPortalServer()) + .addUiPortal('staff', createStaffUiPortalServer()); + +interface InfrastructureState extends E2EInfrastructureState { + staffBaseUrl: string | undefined; + communityBaseUrl: string | undefined; +} + +export function getState(): InfrastructureState { + const state = infrastructure.getState(); + return { + ...state, + communityBaseUrl: state.uiPortalBaseUrls['community'], + staffBaseUrl: state.uiPortalBaseUrls['staff'], + }; +} + +export async function resetScenarioState(): Promise { + await infrastructure.resetScenarioState(); +} + +export async function stopAll(): Promise { + await infrastructure.stopAll(); +} + +export async function ensureE2EServers(): Promise { + infrastructure.registerProcessShutdownHandlers(); + await infrastructure.ensureStarted(); +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/cast.ts b/packages/ocom-verification/e2e-tests/src/shared/support/cast.ts deleted file mode 100644 index a88926e1c..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/cast.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type Actor, type Cast, Notepad, TakeNotes } from '@serenity-js/core'; -import type { BrowseTheWeb } from '../abilities/browse-the-web.ts'; - -export class CellixE2ECast implements Cast { - constructor(private readonly browseTheWeb?: BrowseTheWeb) {} - - prepare(actor: Actor): Actor { - if (!this.browseTheWeb) { - throw new Error('E2E tests require a browser'); - } - return actor.whoCan(TakeNotes.using(Notepad.empty()), this.browseTheWeb); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts deleted file mode 100644 index 477877fc7..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/hooks.ts +++ /dev/null @@ -1,47 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { ITestCaseHookParameter, IWorld } from '@cucumber/cucumber'; -import { After, AfterAll, Before, Status, setDefaultTimeout } from '@cucumber/cucumber'; -import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { type CellixE2EWorld, stopSharedServers } from '../../world.ts'; -import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; - -const currentDir = fileURLToPath(new URL('.', import.meta.url)); - -/** Default scenario timeout from centralized configuration */ -setDefaultTimeout(getTimeout('scenario')); - -Before(async function (this: IWorld) { - const world = this as IWorld & CellixE2EWorld; - await world.init(); -}); - -After(async function (this: IWorld, { result, pickle }: ITestCaseHookParameter) { - const world = this as IWorld & CellixE2EWorld; - - if (result?.status === Status.FAILED) { - try { - const browseTheWeb = BrowseTheWeb.current(); - if (browseTheWeb) { - const reportsDir = path.resolve(currentDir, '..', '..', '..', 'reports', 'screenshots'); - fs.mkdirSync(reportsDir, { recursive: true }); - - const safeName = pickle.name.replaceAll(/[^a-zA-Z0-9-_]/g, '_').slice(0, 80); - const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-'); - const screenshotPath = path.join(reportsDir, `${safeName}-${timestamp}.png`); - - await browseTheWeb.page.screenshot({ path: screenshotPath, fullPage: true }); - this.attach(fs.readFileSync(screenshotPath), 'image/png'); - } - } catch { - /* Screenshot capture is best-effort */ - } - } - - await world.cleanup(); -}); - -AfterAll(async () => { - await stopSharedServers(); -}); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts b/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts deleted file mode 100644 index 92911f16d..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/oauth2-login.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { TaskStep } from '@ocom-verification/verification-shared/serenity'; -import { actors } from '@ocom-verification/verification-shared/test-data'; -import { type Activity, type Actor, Task, the } from '@serenity-js/core'; -import type { Page } from 'playwright'; -import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; - -/** - * URL predicate that resolves once the OIDC redirect chain has settled — - * i.e. we are no longer on the mock-auth hostname or the /auth-redirect - * callback path. - */ -const isPostAuthUrl = (url: URL) => !url.hostname.includes('mock-auth') && !url.pathname.includes('auth-redirect'); - -/** - * Authenticates the browser session via the OIDC auto-redirect flow. - * - * The app uses RequireAuth + react-oidc-context. When an unauthenticated - * user hits a protected route, RequireAuth calls `signinRedirect()` which - * navigates to the mock OAuth2 server's `/authorize` endpoint. The mock - * server redirects to `/login` (since userStore is configured). This - * function fills in the test user credentials and submits the form. - */ -export async function performOAuth2Login(page: Page): Promise { - // Navigate to a protected route to trigger the OIDC signinRedirect flow. - try { - await page.goto('/community/accounts', { - waitUntil: 'networkidle', - timeout: 60_000, - }); - } catch { - // Navigation may be interrupted by OIDC redirect — this is expected - } - - // Wait for redirects to settle on either the login page or the app - await page.waitForLoadState('domcontentloaded', { timeout: 10_000 }).catch(() => undefined); - - // If the mock OAuth2 login form is shown, fill credentials and submit. - // CommunityOwner is defined in mock-oidc.users.json with password "password". - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', actors.CommunityOwner.email); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - } - - // Wait for the redirect chain to settle on an authenticated page - await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); - await page.waitForLoadState('networkidle'); -} - -/** - * Screenplay Task — confirms the actor is authenticated. - * - * The browser context is pre-authenticated by {@link performOAuth2Login} - * during server setup. This task navigates to a protected route and - * verifies the page loads without being kicked to the auth provider. - */ -export const OAuth2Login = (_email?: string, _password?: string) => - Task.where( - the`#actor logs in via OAuth2`, - new TaskStep('#actor confirms the OAuth2 session is active', async (actor) => { - const { page } = BrowseTheWeb.withActor(actor as Actor); - - // Session tokens live in sessionStorage from pre-auth. - try { - await page.goto('/community/accounts', { - waitUntil: 'networkidle', - timeout: 30_000, - }); - } catch { - // Navigation may be interrupted by OIDC redirect on first access - } - - if (page.url().includes('/login')) { - await page.fill('input[name="username"]', actors.CommunityOwner.email); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - } - - await page.waitForURL(isPostAuthUrl, { timeout: 30_000 }); - }) as Activity, - ); diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts deleted file mode 100644 index 1ce570cd1..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/child-process-env.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function spawnEnv(overrides: Record = {}): NodeJS.ProcessEnv { - const { NODE_OPTIONS: _ignored, ...baseEnv } = process.env; - return { ...baseEnv, ...overrides }; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts deleted file mode 100644 index 3f7b41971..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { MongoDBTestServer } from '@ocom-verification/verification-shared/servers'; -export { PortlessServer } from './portless-server.ts'; -export { TestApiServer } from './test-api-server.ts'; -export { TestAzuriteServer } from './test-azurite-server.ts'; -export { TestCommunityViteServer } from './test-community-vite-server.ts'; -export { - buildUrl, - cleanupTestEnvironment, - initTestEnvironment, - mockOidcAudience, - mockOidcEndpoint, - mockOidcIssuer, - mockStaffOidcIssuer, - setMongoConnectionString, -} from './test-environment.ts'; -export { TestOAuth2Server } from './test-oauth2-server.ts'; -export { TestStaffViteServer } from './test-staff-vite-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts deleted file mode 100644 index feb84d100..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/portless-server.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { type ChildProcess, spawn } from 'node:child_process'; -import type { TestServer } from '@ocom-verification/verification-shared/servers'; -import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { spawnEnv } from './child-process-env.ts'; -import { getPortlessPath } from './resolve-portless.ts'; - -/** - * Abstract base class for portless-proxied servers. - * Subclasses define the hostname, command, ready marker, probe URL, and working directory. - */ -export abstract class PortlessServer implements TestServer { - private process: ChildProcess | null = null; - private startedByUs = false; - private readonly useDetachedProcessGroup = process.platform !== 'win32'; - - protected abstract get probeUrl(): string; - protected abstract get readyMarker(): string; - protected abstract get serverName(): string; - protected abstract get spawnArgs(): string[]; - protected abstract get cwd(): string; - - protected get executable(): string { - return getPortlessPath(); - } - - protected get probeRequestInit(): RequestInit { - return {}; - } - - protected get extraEnv(): Record { - return {}; - } - - protected get startupTimeoutMs(): number { - return getTimeout('serverStartup'); - } - - protected isProbeHealthy(response: Response): boolean | Promise { - return response.ok; - } - - isAlreadyRunning(): Promise { - return this.isProbeReadyWithin(getTimeout('healthProbe')); - } - - abstract getUrl(): string; - - async start(): Promise { - if (this.process || this.startedByUs) return; - if (await this.isAlreadyRunning()) return; - - this.process = spawn(this.executable, this.spawnArgs, { - cwd: this.cwd, - env: spawnEnv(this.extraEnv), - detached: this.useDetachedProcessGroup, - stdio: ['ignore', 'pipe', 'pipe'], - }); - this.startedByUs = true; - - await this.waitForReady(); - } - - async stop(): Promise { - if (!this.process || !this.startedByUs) return; - - const proc = this.process; - this.process = null; - this.startedByUs = false; - - this.killProcess(proc, 'SIGINT'); - - const shutdownTimeout = getTimeout('serverShutdown'); - await new Promise((resolve) => { - const timeout = setTimeout(() => { - this.killProcess(proc, 'SIGKILL'); - resolve(); - }, shutdownTimeout); - - proc.on('exit', () => { - clearTimeout(timeout); - resolve(); - }); - }); - } - - isRunning(): boolean { - return this.process !== null; - } - - private waitForReady(): Promise { - return new Promise((resolve, reject) => { - const proc = this.process; - if (!proc) { - reject(new Error(`${this.serverName} process not started`)); - return; - } - - const startupTimeout = this.startupTimeoutMs; - const startupDeadline = Date.now() + startupTimeout; - const timeout = setTimeout(() => { - reject(new Error(`${this.serverName} did not start within ${startupTimeout}ms`)); - }, startupTimeout); - - let stderrOutput = ''; - let ready = false; - - const resolveWhenReachable = () => { - if (ready) { - return; - } - ready = true; - - this.waitForProbeReady(startupDeadline, startupTimeout) - .then(() => { - clearTimeout(timeout); - resolve(); - }) - .catch((error: unknown) => { - clearTimeout(timeout); - reject(error); - }); - }; - - proc.stdout?.on('data', (data: Buffer) => { - const text = data.toString(); - if (text.includes(this.readyMarker)) { - resolveWhenReachable(); - } - }); - - proc.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); - }); - - proc.on('error', (err: Error) => { - clearTimeout(timeout); - this.process = null; - this.startedByUs = false; - reject(new Error(`${this.serverName} failed to start: ${err.message}`)); - }); - - proc.on('exit', (code, signal) => { - if (ready) return; - clearTimeout(timeout); - this.process = null; - this.startedByUs = false; - reject(new Error(`${this.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); - }); - }); - } - - private async waitForProbeReady(startupDeadline: number, startupTimeout: number): Promise { - const probeInterval = getTimeout('healthProbeInterval'); - const timeoutError = () => new Error(`${this.serverName} did not become healthy within ${startupTimeout}ms`); - - while (true) { - const remainingMs = startupDeadline - Date.now(); - if (remainingMs <= 0) { - throw timeoutError(); - } - - if (await this.isProbeReadyWithin(Math.min(getTimeout('healthProbe'), remainingMs))) { - return; - } - - const retryDelay = Math.min(probeInterval, startupDeadline - Date.now()); - if (retryDelay <= 0) { - throw timeoutError(); - } - - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - } - - private async isProbeReadyWithin(timeoutMs: number): Promise { - let timeout: ReturnType | undefined; - try { - const controller = new AbortController(); - timeout = setTimeout(() => controller.abort(), timeoutMs); - const response = await fetch(this.probeUrl, { ...this.probeRequestInit, signal: controller.signal }); - return await this.isProbeHealthy(response); - } catch { - return false; - } finally { - if (timeout) clearTimeout(timeout); - } - } - - private killProcess(proc: ChildProcess, signal: NodeJS.Signals): void { - if (this.useDetachedProcessGroup && proc.pid) { - try { - process.kill(-proc.pid, signal); - return; - } catch { - /* Fall back to killing the direct child below. */ - } - } - - proc.kill(signal); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts deleted file mode 100644 index ea701b158..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-api-server.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { appPaths } from './app-paths.ts'; -import { e2eEnv, getPortlessDevScript } from './dev-script.ts'; -import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getHostnames, getMongoConnectionString } from './test-environment.ts'; - -const hostnames = getHostnames(); - -/** - * Spawns the api e2e dev server through the PR's portless/worktree path. - */ -export class TestApiServer extends PortlessServer { - protected get probeUrl() { - return this.getUrl(); - } - - protected override get probeRequestInit(): RequestInit { - return { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: '{ __typename }' }), - }; - } - - protected override async isProbeHealthy(response: Response): Promise { - if (!response.ok) { - return false; - } - - const payload = (await response.json().catch(() => null)) as { - data?: { __typename?: string }; - errors?: unknown[]; - } | null; - - return payload?.data?.__typename === 'Query' && !payload.errors?.length; - } - - protected get readyMarker() { - return 'Functions:'; - } - - protected get serverName() { - return 'TestApiServer'; - } - - protected override get executable() { - return 'pnpm'; - } - - protected get spawnArgs() { - return ['run', getPortlessDevScript()]; - } - - protected get cwd() { - return appPaths.apiDir; - } - - protected override get extraEnv() { - return e2eEnv({ - COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - }); - } - - getUrl(): string { - return buildUrl(hostnames.api, '/api/graphql'); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts deleted file mode 100644 index 9f3c8f28b..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-azurite-server.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { type ChildProcess, spawn } from 'node:child_process'; -import { join } from 'node:path'; -import type { TestServer } from '@ocom-verification/verification-shared/servers'; -import { getTimeout } from '@ocom-verification/verification-shared/settings'; -import { appPaths } from './app-paths.ts'; -import { spawnEnv } from './child-process-env.ts'; -import { getAzuritePorts } from './worktree-ports.ts'; - -/** - * Starts Azurite via apps/api/start-azurite.mjs. - * If ports are already bound (EADDRINUSE), we treat that as an existing - * reusable instance for this worktree. - */ -export class TestAzuriteServer implements TestServer { - private process: ChildProcess | null = null; - private startedByUs = false; - private readonly useDetachedProcessGroup = process.platform !== 'win32'; - - private get blobPort(): number { - return getAzuritePorts().blob; - } - - async start(): Promise { - if (this.process || this.startedByUs) return; - - const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); - const { PATH: pathValue = '' } = process.env; - - this.process = spawn('node', ['start-azurite.mjs'], { - cwd: appPaths.apiDir, - env: spawnEnv({ PATH: `${binDir}:${pathValue}` }), - detached: this.useDetachedProcessGroup, - stdio: ['ignore', 'pipe', 'pipe'], - }); - this.startedByUs = true; - - await this.waitForStartedMarker(); - } - - async stop(): Promise { - if (!this.process || !this.startedByUs) return; - - const proc = this.process; - this.process = null; - this.startedByUs = false; - - killProcess(proc, 'SIGTERM', this.useDetachedProcessGroup); - - await new Promise((resolve) => { - const timeout = setTimeout(() => { - killProcess(proc, 'SIGKILL', this.useDetachedProcessGroup); - resolve(); - }, getTimeout('serverShutdown')); - - proc.on('exit', () => { - clearTimeout(timeout); - resolve(); - }); - }); - } - - isRunning(): boolean { - return this.process !== null; - } - - getUrl(): string { - return `http://127.0.0.1:${this.blobPort}`; - } - - private waitForStartedMarker(): Promise { - return new Promise((resolve, reject) => { - const proc = this.process; - if (!proc) { - reject(new Error('TestAzuriteServer process not started')); - return; - } - - const timeout = setTimeout(() => { - reject(new Error(`TestAzuriteServer did not emit start marker within ${getTimeout('serverStartup')}ms`)); - }, getTimeout('serverStartup')); - - let stderrOutput = ''; - - proc.stdout?.on('data', (data: Buffer) => { - if (data.toString().includes('[azurite] started')) { - clearTimeout(timeout); - resolve(); - } - }); - - proc.stderr?.on('data', (data: Buffer) => { - stderrOutput += data.toString(); - }); - - proc.on('error', (error: Error) => { - clearTimeout(timeout); - reject(new Error(`TestAzuriteServer failed to start: ${error.message}`)); - }); - - proc.on('exit', (code, signal) => { - clearTimeout(timeout); - if (stderrOutput.includes('EADDRINUSE')) { - this.process = null; - this.startedByUs = false; - resolve(); - return; - } - reject(new Error(`TestAzuriteServer exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); - }); - }); - } -} - -function killProcess(proc: ChildProcess, signal: NodeJS.Signals, useGroup: boolean): void { - if (useGroup && proc.pid) { - try { - process.kill(-proc.pid, signal); - return; - } catch { - /* Fall back to killing the direct child. */ - } - } - proc.kill(signal); -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts deleted file mode 100644 index fb567e0aa..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-community-vite-server.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { appPaths } from './app-paths.ts'; -import { e2eEnv, getPortlessDevScript } from './dev-script.ts'; -import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getHostnames } from './test-environment.ts'; - -const hostnames = getHostnames(); - -/** - * Starts the community portal Vite dev server via portless. - */ -export class TestCommunityViteServer extends PortlessServer { - protected get probeUrl() { - return this.getUrl(); - } - - protected get readyMarker() { - return 'ready in'; - } - - protected get serverName() { - return 'TestCommunityViteServer'; - } - - protected override get executable() { - return 'pnpm'; - } - - protected get spawnArgs() { - return ['run', getPortlessDevScript()]; - } - - protected get cwd() { - return appPaths.uiCommunityDir; - } - - protected override get extraEnv() { - return e2eEnv({ - BROWSER: 'none', - NODE_ENV: 'development', - }); - } - - getUrl(): string { - return buildUrl(hostnames.uiCommunity); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts deleted file mode 100644 index f93859c16..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-environment.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { execFileSync } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { buildPortlessUrl, getHostnames } from '@ocom-verification/verification-shared/settings'; -import { getPortlessPath } from './resolve-portless.ts'; - -let proxyInitialized = false; -let mongoConnectionString: string | undefined; - -loadE2EEnvDefaults(); - -const hostnames = getHostnames(); - -export const mockOidcAudience = 'mock-client'; -export const mockOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/community'); -export const mockOidcEndpoint = `${mockOidcIssuer}/.well-known/jwks.json`; -export const mockStaffOidcIssuer = buildPortlessUrl(hostnames.mockAuth, '/staff'); - -/** - * Ensure the portless proxy is running for the PR's worktree-scoped hostnames. - */ -export function initTestEnvironment() { - if (proxyInitialized) return; - - execFileSync(getPortlessPath(), ['prune'], { - timeout: 10_000, - stdio: 'pipe', - }); - execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { - timeout: 15_000, - stdio: 'pipe', - }); - - proxyInitialized = true; -} - -export { buildPortlessUrl as buildUrl, getHostnames }; - -export function setMongoConnectionString(connStr: string): void { - mongoConnectionString = connStr; -} - -export function getMongoConnectionString(): string { - if (!mongoConnectionString) { - throw new Error('MongoDB connection string not set - call setMongoConnectionString() first'); - } - return mongoConnectionString; -} - -export function cleanupTestEnvironment(): void { - proxyInitialized = false; - mongoConnectionString = undefined; -} - -function loadE2EEnvDefaults(): void { - const currentDir = dirname(fileURLToPath(import.meta.url)); - const workspaceRoot = resolve(currentDir, '../../../../../../..'); - for (const filePath of [resolve(workspaceRoot, 'apps/ui-community/.env.e2e'), resolve(workspaceRoot, 'apps/ui-staff/.env.e2e')]) { - if (!existsSync(filePath)) continue; - for (const line of readFileSync(filePath, 'utf-8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const idx = trimmed.indexOf('='); - if (idx === -1) continue; - const key = trimmed.slice(0, idx); - process.env[key] ??= trimmed.slice(idx + 1); - } - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts deleted file mode 100644 index 424d028e3..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-oauth2-server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { appPaths } from './app-paths.ts'; -import { getPortlessDevScript } from './dev-script.ts'; -import { PortlessServer } from './portless-server.ts'; -import { mockOidcEndpoint, mockOidcIssuer } from './test-environment.ts'; - -/** - * Starts the mock OAuth2/OIDC server via portless. - */ -export class TestOAuth2Server extends PortlessServer { - protected get probeUrl() { - return mockOidcEndpoint; - } - - protected get readyMarker() { - return 'Registered OIDC config'; - } - - protected get serverName() { - return 'TestOAuth2Server'; - } - - protected override get executable() { - return 'pnpm'; - } - - protected get spawnArgs() { - return ['run', getPortlessDevScript()]; - } - - protected get cwd() { - return appPaths.oauth2MockDir; - } - - getUrl(): string { - return mockOidcIssuer; - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts b/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts deleted file mode 100644 index 8eab2f068..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/servers/test-staff-vite-server.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { appPaths } from './app-paths.ts'; -import { e2eEnv, getPortlessDevScript } from './dev-script.ts'; -import { PortlessServer } from './portless-server.ts'; -import { buildUrl, getHostnames } from './test-environment.ts'; - -const hostnames = getHostnames(); - -/** - * Starts the staff portal Vite dev server via portless. - */ -export class TestStaffViteServer extends PortlessServer { - protected get probeUrl() { - return this.getUrl(); - } - - protected get readyMarker() { - return 'ready in'; - } - - protected get serverName() { - return 'TestStaffViteServer'; - } - - protected override get executable() { - return 'pnpm'; - } - - protected get spawnArgs() { - return ['run', getPortlessDevScript()]; - } - - protected get cwd() { - return appPaths.uiStaffDir; - } - - protected override get extraEnv() { - return e2eEnv({ - BROWSER: 'none', - NODE_ENV: 'development', - }); - } - - getUrl(): string { - return buildUrl(hostnames.uiStaff); - } -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts deleted file mode 100644 index 472301f78..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/support/shared-infrastructure.ts +++ /dev/null @@ -1,190 +0,0 @@ -import playwright, { type Browser, type BrowserContext } from 'playwright'; -import { BrowseTheWeb } from '../abilities/browse-the-web.ts'; -import { performOAuth2Login } from './oauth2-login.ts'; -import { cleanupTestEnvironment, initTestEnvironment, MongoDBTestServer, setMongoConnectionString, TestApiServer, TestAzuriteServer, TestCommunityViteServer, TestOAuth2Server, TestStaffViteServer } from './servers/index.ts'; -import { getMongoPort } from './servers/worktree-ports.ts'; - -const apiDbName = 'owner-community'; - -let mongoDBServer: MongoDBTestServer | undefined; -let azuriteServer: TestAzuriteServer | undefined; -let oauth2Server: TestOAuth2Server | undefined; -let apiServer: TestApiServer | undefined; -let communityViteServer: TestCommunityViteServer | undefined; -let staffViteServer: TestStaffViteServer | undefined; -let apiUrl: string | undefined; -let browser: Browser | undefined; -let browserBaseUrl: string | undefined; -let authenticatedBrowserContext: BrowserContext | undefined; -let browseTheWeb: BrowseTheWeb | undefined; -let shutdownHandlersRegistered = false; - -export interface InfrastructureState { - apiUrl: string | undefined; - browseTheWeb: BrowseTheWeb | undefined; - staffBaseUrl: string | undefined; - communityBaseUrl: string | undefined; - browser: Browser | undefined; -} - -export function getState(): InfrastructureState { - return { apiUrl, browseTheWeb, staffBaseUrl: staffViteServer?.getUrl(), communityBaseUrl: browserBaseUrl, browser }; -} - -/** - * Resets mutable state between scenarios without restarting servers. - */ -export async function resetScenarioState(): Promise { - if (mongoDBServer?.isRunning()) { - await mongoDBServer.resetForScenario(); - } -} - -export async function stopAll(): Promise { - if (browseTheWeb) { - await browseTheWeb.close().catch(() => undefined); - browseTheWeb = undefined; - } else if (authenticatedBrowserContext) { - await authenticatedBrowserContext.close().catch(() => undefined); - } - authenticatedBrowserContext = undefined; - - if (browser) { - await browser.close().catch(() => undefined); - browser = undefined; - } - if (communityViteServer) { - await communityViteServer.stop().catch(() => undefined); - communityViteServer = undefined; - } - if (staffViteServer) { - await staffViteServer.stop().catch(() => undefined); - staffViteServer = undefined; - } - if (apiServer) { - await apiServer.stop().catch(() => undefined); - apiServer = undefined; - } - if (oauth2Server) { - await oauth2Server.stop().catch(() => undefined); - oauth2Server = undefined; - } - if (mongoDBServer) { - await mongoDBServer.stop().catch(() => undefined); - mongoDBServer = undefined; - } - if (azuriteServer) { - await azuriteServer.stop().catch(() => undefined); - azuriteServer = undefined; - } - - apiUrl = undefined; - browserBaseUrl = undefined; - cleanupTestEnvironment(); -} - -export async function ensureE2EServers(): Promise { - initTestEnvironment(); - registerShutdownHandlers(); - - mongoDBServer ??= new MongoDBTestServer(); - azuriteServer ??= new TestAzuriteServer(); - oauth2Server ??= new TestOAuth2Server(); - - const mongo = mongoDBServer; - const azurite = azuriteServer; - const oauth2 = oauth2Server; - const phase1: Promise[] = []; - - if (!mongo.isRunning()) { - phase1.push( - mongo.start({ dbName: apiDbName, port: getMongoPort() }).then(() => { - setMongoConnectionString(mongo.getConnectionString()); - }), - ); - } - if (!azurite.isRunning()) { - phase1.push(azurite.start()); - } - if (!oauth2.isRunning()) { - phase1.push(oauth2.start()); - } - if (phase1.length > 0) await Promise.all(phase1); - - apiServer ??= new TestApiServer(); - communityViteServer ??= new TestCommunityViteServer(); - staffViteServer ??= new TestStaffViteServer(); - - const api = apiServer; - const communityVite = communityViteServer; - const staffVite = staffViteServer; - const phase2: Promise[] = []; - - if (!api.isRunning()) { - phase2.push( - api.start().then(() => { - apiUrl = api.getUrl(); - }), - ); - } - if (!communityVite.isRunning()) { - phase2.push(communityVite.start()); - } - if (!staffVite.isRunning()) { - phase2.push(staffVite.start()); - } - if (phase2.length > 0) await Promise.all(phase2); - - browserBaseUrl = communityVite.getUrl(); - apiUrl ??= api.getUrl(); - - if (!browser) { - browser = await playwright.chromium.launch({ headless: true }); - } - - await ensureAuthenticatedBrowserContext({ - baseURL: browserBaseUrl, - ignoreHTTPSErrors: true, - performLogin: true, - }); -} - -async function ensureAuthenticatedBrowserContext(options: { baseURL?: string; ignoreHTTPSErrors: boolean; performLogin: boolean }): Promise { - if (browseTheWeb || !browser || !options.baseURL) { - return; - } - - if (!authenticatedBrowserContext) { - authenticatedBrowserContext = await browser.newContext({ - baseURL: options.baseURL, - ignoreHTTPSErrors: options.ignoreHTTPSErrors, - }); - } - - const seedPage = await authenticatedBrowserContext.newPage(); - - try { - if (options.performLogin) { - await performOAuth2Login(seedPage); - } - browseTheWeb = BrowseTheWeb.using(seedPage, authenticatedBrowserContext); - } catch (error) { - await authenticatedBrowserContext.close().catch(() => undefined); - authenticatedBrowserContext = undefined; - throw error; - } -} - -function registerShutdownHandlers(): void { - if (shutdownHandlersRegistered) return; - shutdownHandlersRegistered = true; - - const shutdown = (signal: string) => { - void stopAll().finally(() => { - process.exit(signal === 'SIGINT' ? 130 : 143); - }); - }; - - process.once('SIGINT', () => shutdown('SIGINT')); - process.once('SIGTERM', () => shutdown('SIGTERM')); -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts b/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts new file mode 100644 index 000000000..3c51cb5c0 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts @@ -0,0 +1,108 @@ +import { join } from 'node:path'; +import { ApiTestServer, AuthTestServer, AzuriteTestServer, UiPortalTestServer } from '@cellix/serenity-framework/servers'; +import { appPaths } from './environment/app-paths.ts'; +import { e2eEnv, getPortlessDevScript } from './environment/dev-script.ts'; +import { buildUrl, cleanupTestEnvironment, getHostnames, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer } from './environment/test-environment.ts'; +import { getAzuritePorts } from './environment/worktree-ports.ts'; + +const hostnames = getHostnames(); + +export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer }; + +export function createTestApiServer(getMongoConnectionString: () => string): ApiTestServer { + return new ApiTestServer({ + cwd: appPaths.apiDir, + executable: 'pnpm', + extraEnv: () => + e2eEnv({ + COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), + }), + getUrl: () => buildUrl(hostnames.api, '/api/graphql'), + probe: { + url: () => buildUrl(hostnames.api, '/api/graphql'), + requestInit: () => ({ + body: JSON.stringify({ query: '{ __typename }' }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }), + isHealthy: async (response) => { + if (!response.ok) { + return false; + } + + const payload = (await response.json().catch(() => null)) as { + data?: { __typename?: string }; + errors?: unknown[]; + } | null; + + return payload?.data?.__typename === 'Query' && !payload.errors?.length; + }, + }, + readyMarker: 'Functions:', + serverName: 'TestApiServer', + spawnArgs: () => ['run', getPortlessDevScript()], + }); +} + +export function createTestAzuriteServer(): AzuriteTestServer { + return new AzuriteTestServer({ + cwd: appPaths.apiDir, + executable: 'node', + extraEnv: () => { + const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); + const { PATH: pathValue = '' } = process.env; + return { PATH: `${binDir}:${pathValue}` }; + }, + getUrl: () => `http://127.0.0.1:${getAzuritePorts().blob}`, + isAlreadyRunning: async () => false, + isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), + probe: false, + readyMarker: '[azurite] started', + serverName: 'TestAzuriteServer', + spawnArgs: ['start-azurite.mjs'], + }); +} + +export function createTestOAuth2Server(): AuthTestServer { + return new AuthTestServer({ + cwd: appPaths.oauth2MockDir, + executable: 'pnpm', + getUrl: () => mockOidcIssuer, + probe: { + url: mockOidcEndpoint, + }, + readyMarker: 'Registered OIDC config', + serverName: 'TestOAuth2Server', + spawnArgs: () => ['run', getPortlessDevScript()], + }); +} + +export function createCommunityUiPortalServer(): UiPortalTestServer { + return new UiPortalTestServer({ + cwd: appPaths.uiCommunityDir, + executable: 'pnpm', + extraEnv: () => ({ + BROWSER: 'none', + NODE_ENV: 'development', + }), + getUrl: () => buildUrl(hostnames.uiCommunity), + readyMarker: 'ready in', + serverName: 'TestCommunityViteServer', + spawnArgs: () => ['run', getPortlessDevScript()], + }); +} + +export function createStaffUiPortalServer(): UiPortalTestServer { + return new UiPortalTestServer({ + cwd: appPaths.uiStaffDir, + executable: 'pnpm', + extraEnv: () => ({ + BROWSER: 'none', + NODE_ENV: 'development', + }), + getUrl: () => buildUrl(hostnames.uiStaff), + readyMarker: 'ready in', + serverName: 'TestStaffViteServer', + spawnArgs: () => ['run', getPortlessDevScript()], + }); +} diff --git a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts index fb2ff8a4f..1b7c8d7b3 100644 --- a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts +++ b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts @@ -3,6 +3,6 @@ * Cucumber imports this file, which then loads all context-specific step definitions. */ -import '../shared/support/hooks.ts'; +import '../shared/cucumber-lifecycle-hooks.ts'; import '../contexts/community/step-definitions/index.ts'; import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/world.ts b/packages/ocom-verification/e2e-tests/src/world.ts index e40e2eb21..b9da6e5e8 100644 --- a/packages/ocom-verification/e2e-tests/src/world.ts +++ b/packages/ocom-verification/e2e-tests/src/world.ts @@ -1,30 +1,38 @@ -import { setWorldConstructor, World } from '@cucumber/cucumber'; -import { engage } from '@serenity-js/core'; -import './shared/support/hooks.ts'; -import { CellixE2ECast } from './shared/support/cast.ts'; -import * as infra from './shared/support/shared-infrastructure.ts'; +import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; +import './shared/cucumber-lifecycle-hooks.ts'; +import { OAuth2Login } from './shared/abilities/oauth2-login.ts'; +import * as infra from './shared/shared-infrastructure.ts'; export async function stopSharedServers(): Promise { await infra.stopAll(); } -export class CellixE2EWorld extends World { - async init(): Promise { - await infra.ensureE2EServers(); - - const { browseTheWeb } = infra.getState(); - if (!browseTheWeb) { +export const CellixE2EWorld = registerManagedSerenityWorld({ + infrastructure: { + ensureStarted: infra.ensureE2EServers, + getState: infra.getState, + resetScenarioState: infra.resetScenarioState, + stopAll: infra.stopAll, + }, + validateState: (state) => { + if (!state.browseTheWeb) { throw new Error('BrowseTheWeb ability not initialized'); } + }, + createCast: (state) => + new SerenityCast({ + useNotepad: true, + abilities: [ + () => { + if (!state.browseTheWeb) { + throw new Error('BrowseTheWeb ability not initialized'); + } + return state.browseTheWeb; + }, + () => OAuth2Login.throughProtectedRoute('/community/accounts'), + ], + }), +}); - engage(new CellixE2ECast(browseTheWeb)); - } - - async cleanup(): Promise { - // Reset DB state between scenarios so each starts from a clean baseline. - // Servers stay running — only mutable data is cleared and re-seeded. - await infra.resetScenarioState(); - } -} - -setWorldConstructor(CellixE2EWorld); +export type CellixE2EWorld = InstanceType; diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index 9d4803c01..e135dfb0f 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -1,41 +1,22 @@ { "name": "@ocom-verification/verification-shared", "version": "1.0.0", - "description": "Shared Serenity verification utilities, servers, scenarios, and helpers", + "description": "OCOM verification test data and shared Cucumber scenarios", "private": true, "type": "module", "exports": { - "./test-data": "./src/test-data/index.ts", - "./helpers": "./src/helpers/index.ts", - "./formatters": "./src/formatters/index.ts", - "./servers": "./src/servers/index.ts", - "./settings": "./src/settings/index.ts", - "./serenity": "./src/serenity/index.ts", + "./abilities": "./src/abilities/index.ts", "./pages": "./src/pages/index.ts", - "./pages/jsdom": "./src/pages/adapters/jsdom-adapter.ts", - "./pages/playwright": "./src/pages/adapters/playwright-adapter.ts" + "./test-data": "./src/test-data/index.ts" }, "dependencies": { - "@apollo/server": "catalog:", - "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", - "@ocom/service-mongoose": "workspace:*", - "@cucumber/cucumber": "catalog:", - "@cucumber/messages": "catalog:", + "@cellix/serenity-framework": "workspace:*", "@serenity-js/core": "catalog:", - "@ocom/graphql": "workspace:*", - "@ocom/application-services": "workspace:*", - "@testing-library/react": "^16.3.0", - "graphql": "catalog:", - "graphql-depth-limit": "^1.1.0", - "graphql-middleware": "^6.1.35", - "mongodb": "catalog:", - "mongoose": "catalog:" + "mongodb": "catalog:" }, "devDependencies": { "@cellix/config-typescript": "workspace:*", - "@types/graphql-depth-limit": "^1.1.6", "@types/node": "catalog:", - "playwright": "catalog:", "typescript": "catalog:" } } diff --git a/packages/ocom-verification/verification-shared/src/abilities/create-community.ts b/packages/ocom-verification/verification-shared/src/abilities/create-community.ts new file mode 100644 index 000000000..a53c51cb7 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/abilities/create-community.ts @@ -0,0 +1,53 @@ +import { Ability, type Actor } from '@serenity-js/core'; + +/** Community details accepted by OCOM verification flows. */ +export interface CreateCommunityDetails { + /** Community display name. */ + name: string; +} + +/** Result returned by a concrete community creation flow. */ +export interface CreateCommunityResult { + /** Created community id, when the flow exposes one. */ + id?: string; + + /** Created community name. */ + name: string; +} + +/** Handler that performs community creation for an actor. */ +export type CreateCommunityHandler = (actor: Actor, details: CreateCommunityDetails) => Promise; + +/** + * Serenity ability that lets an actor create OCOM communities. + * + * The ability centralizes the domain capability while allowing each verification + * package to provide the environment-specific implementation. + */ +export class CreateCommunity extends Ability { + /** + * @param handler Function that performs community creation. + */ + constructor(private readonly handler: CreateCommunityHandler) { + super(); + } + + /** + * Create the ability from an environment-specific community creation handler. + * + * @param handler Function that performs community creation. + */ + static using(handler: CreateCommunityHandler): CreateCommunity { + return new CreateCommunity(handler); + } + + /** + * Create a community through the configured verification environment. + * + * @param actor Actor creating the community. + * @param details Community details. + */ + async performAs(actor: Actor, details: CreateCommunityDetails): Promise { + return await this.handler(actor, details); + } +} diff --git a/packages/ocom-verification/verification-shared/src/abilities/index.ts b/packages/ocom-verification/verification-shared/src/abilities/index.ts new file mode 100644 index 000000000..65388a4ab --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/abilities/index.ts @@ -0,0 +1,2 @@ +export type { CreateCommunityDetails, CreateCommunityHandler, CreateCommunityResult } from './create-community.ts'; +export { CreateCommunity } from './create-community.ts'; diff --git a/packages/ocom-verification/verification-shared/src/formatters/index.ts b/packages/ocom-verification/verification-shared/src/formatters/index.ts deleted file mode 100644 index a75c8ca6b..000000000 --- a/packages/ocom-verification/verification-shared/src/formatters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default, default as AgentFormatter } from './agent-formatter.ts'; diff --git a/packages/ocom-verification/verification-shared/src/helpers/actor-helpers.ts b/packages/ocom-verification/verification-shared/src/helpers/actor-helpers.ts deleted file mode 100644 index 88c35b0dc..000000000 --- a/packages/ocom-verification/verification-shared/src/helpers/actor-helpers.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface ActorDetails { - name: string; - externalId: string; - email: string; - givenName: string; - familyName: string; -} - -// Resolve Gherkin pronoun references to actor names -export function resolveActorName(actorName: string, defaultName = 'Alice'): string { - return /^(she|he|they)$/i.test(actorName) ? defaultName : actorName; -} diff --git a/packages/ocom-verification/verification-shared/src/helpers/date-helpers.ts b/packages/ocom-verification/verification-shared/src/helpers/date-helpers.ts deleted file mode 100644 index da0a6b8f6..000000000 --- a/packages/ocom-verification/verification-shared/src/helpers/date-helpers.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const ONE_DAY_MS = 86_400_000; - -export function parseDateInput(input: string): Date { - if (input.startsWith('+')) { - const days = Number.parseInt(input.substring(1), 10); - const date = new Date(); - date.setDate(date.getDate() + days); - date.setHours(0, 0, 0, 0); - return date; - } - - const date = new Date(input); - if (Number.isNaN(date.getTime())) { - throw new TypeError(`Invalid date input: "${input}"`); - } - date.setHours(0, 0, 0, 0); - return date; -} - -export function formatDateForComparison(date: Date): string { - return date.toISOString().split('T')[0] ?? ''; -} diff --git a/packages/ocom-verification/verification-shared/src/helpers/gherkin-helpers.ts b/packages/ocom-verification/verification-shared/src/helpers/gherkin-helpers.ts deleted file mode 100644 index db0b9c910..000000000 --- a/packages/ocom-verification/verification-shared/src/helpers/gherkin-helpers.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { DataTable } from '@cucumber/cucumber'; - -/** - * Returns a DataTable's rowsHash typed as the caller-provided shape so consumers - * can access keys directly (e.g. `row.name`) without tripping - * TypeScript's `noPropertyAccessFromIndexSignature` rule. - */ -export function typedRowsHash(dataTable: DataTable): T { - return dataTable.rowsHash() as T; -} diff --git a/packages/ocom-verification/verification-shared/src/helpers/index.ts b/packages/ocom-verification/verification-shared/src/helpers/index.ts deleted file mode 100644 index a9b4a6381..000000000 --- a/packages/ocom-verification/verification-shared/src/helpers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type { ActorDetails } from './actor-helpers.ts'; -export { resolveActorName } from './actor-helpers.ts'; -export { - formatDateForComparison, - ONE_DAY_MS, - parseDateInput, -} from './date-helpers.ts'; -export { typedRowsHash } from './gherkin-helpers.ts'; -export type { TestUserData } from './user-helpers.ts'; -export { makeTestUserData } from './user-helpers.ts'; diff --git a/packages/ocom-verification/verification-shared/src/helpers/user-helpers.ts b/packages/ocom-verification/verification-shared/src/helpers/user-helpers.ts deleted file mode 100644 index f8a92a399..000000000 --- a/packages/ocom-verification/verification-shared/src/helpers/user-helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface TestUserData { - id: string; - email: string; - firstName: string; - lastName: string; -} - -export function makeTestUserData(actorName: string, overrides?: Partial): TestUserData { - const defaultId = `test-user-${actorName.toLowerCase()}`; - const defaultEmail = `${actorName.toLowerCase()}@test.com`; - const defaultFirstName = actorName; - const defaultLastName = 'Tester'; - - return { - id: overrides?.id ?? defaultId, - email: overrides?.email ?? defaultEmail, - firstName: overrides?.firstName ?? defaultFirstName, - lastName: overrides?.lastName ?? defaultLastName, - }; -} diff --git a/packages/ocom-verification/verification-shared/src/pages/community.page.ts b/packages/ocom-verification/verification-shared/src/pages/community.page.ts index 91317f1aa..a9b333811 100644 --- a/packages/ocom-verification/verification-shared/src/pages/community.page.ts +++ b/packages/ocom-verification/verification-shared/src/pages/community.page.ts @@ -1,29 +1,14 @@ -import type { ElementHandle, PageAdapter } from './page-adapter.ts'; +import { AdapterBackedPageObject, type ElementHandle } from '@cellix/serenity-framework/pages'; -/** - * Community page object — works with both jsdom (acceptance UI tests) - * and Playwright (e2e tests) via the PageAdapter abstraction. - */ -export class CommunityPage { - constructor(private readonly adapter: PageAdapter) {} - - // --- Create Community form --- +export class CommunityPage extends AdapterBackedPageObject { get nameInput(): ElementHandle { return this.adapter.getByPlaceholder('Name'); } - get createCommunityButton(): ElementHandle { - return this.adapter.getByRole('button', { name: /Create.*Community/i }); - } - get submitButton(): ElementHandle { return this.adapter.getByRole('button', { name: /Create/i }); } - get cancelButton(): ElementHandle { - return this.adapter.getByRole('button', { name: /Cancel/i }); - } - get firstValidationError(): ElementHandle { return this.adapter.locator('.ant-form-item-explain-error'); } @@ -32,54 +17,11 @@ export class CommunityPage { return this.adapter.locator('.ant-message-error, [role="alert"]'); } - // --- Loading indicator --- - get loadingButton(): ElementHandle { - return this.adapter.locator('.ant-btn-loading'); - } - - // --- Success modal --- - get modal(): ElementHandle { - return this.adapter.locator('.ant-modal'); - } - - get viewCommunityButton(): ElementHandle { - return this.adapter.getByRole('button', { name: /View Community/i }); - } - - // --- Community list table --- - communityNameCell(name: string): ElementHandle { - return this.adapter.getByText(name, { selector: 'table' }); - } - - async statusTagInRow(name: string): Promise { - const row = await this.communityRowByName(name); - return row ? row.querySelector('.ant-tag') : null; - } - - // --- Helper methods --- async fillName(value: string): Promise { await this.nameInput.fill(value); } - async fillForm(data: { name?: string }): Promise { - if (data.name) await this.fillName(data.name); - } - async clickCreate(): Promise { await this.submitButton.click(); } - - private async communityRowByName(name: string): Promise { - const table = this.adapter.getByRole('table'); - const rows = await table.querySelectorAll('tr'); - - for (const row of rows) { - const text = await row.textContent(); - if (text?.includes(name)) { - return row; - } - } - - return null; - } } diff --git a/packages/ocom-verification/verification-shared/src/pages/home.page.ts b/packages/ocom-verification/verification-shared/src/pages/home.page.ts index 435596ca3..1642abb1f 100644 --- a/packages/ocom-verification/verification-shared/src/pages/home.page.ts +++ b/packages/ocom-verification/verification-shared/src/pages/home.page.ts @@ -1,13 +1,6 @@ -import type { ElementHandle, PageAdapter } from './page-adapter.ts'; - -/** - * Home page object — represents the landing screen that contains the - * site header with sign-in controls. Works with both jsdom (acceptance - * UI tests) and Playwright (e2e tests) via the PageAdapter abstraction. - */ -export class HomePage { - constructor(private readonly adapter: PageAdapter) {} +import { AdapterBackedPageObject, type ElementHandle } from '@cellix/serenity-framework/pages'; +export class HomePage extends AdapterBackedPageObject { get signInButton(): ElementHandle { return this.adapter.getByRole('button', { name: /Log In|Sign In/i }); } diff --git a/packages/ocom-verification/verification-shared/src/pages/index.ts b/packages/ocom-verification/verification-shared/src/pages/index.ts index 8c8ff6683..63aa501c8 100644 --- a/packages/ocom-verification/verification-shared/src/pages/index.ts +++ b/packages/ocom-verification/verification-shared/src/pages/index.ts @@ -1,18 +1,2 @@ export { CommunityPage } from './community.page.ts'; export { HomePage } from './home.page.ts'; -export { LoginPage } from './login.page.ts'; -export type { - ElementHandle, - PageAdapter, - PageAdapterMode, - PageNavigationWaitUntil, - PageUrlMatcher, -} from './page-adapter.ts'; -export type { - E2ECommunityPage, - E2EHomePage, - E2ELoginPage, - UiCommunityPage, - UiHomePage, - UiLoginPage, -} from './page-interfaces/index.ts'; diff --git a/packages/ocom-verification/verification-shared/src/pages/login.page.ts b/packages/ocom-verification/verification-shared/src/pages/login.page.ts deleted file mode 100644 index 9e8458800..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/login.page.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { PageAdapter } from './page-adapter.ts'; - -/** - * Shared login page object backed by the universal page adapter. - */ -export class LoginPage { - constructor(private readonly page: PageAdapter) {} - - get emailInput() { - return this.page.getByLabel('Email'); - } - - get passwordInput() { - return this.page.getByLabel('Password'); - } - - get loginButton() { - return this.page.locator('form button[type="submit"]'); - } - - async goto(): Promise { - await this.page.goto('/login', { waitUntil: 'networkidle' }); - } - - async login(email: string, password: string): Promise { - await this.emailInput.fill(email); - await this.passwordInput.fill(password); - await this.loginButton.click(); - } - - async waitForRedirectComplete(): Promise { - await this.page.waitForURL((url) => !url.pathname.includes('auth-redirect') && !url.pathname.includes('/login') && !url.hostname.includes('mock-auth'), { timeout: 30_000 }); - } -} diff --git a/packages/ocom-verification/verification-shared/src/pages/page-adapter.ts b/packages/ocom-verification/verification-shared/src/pages/page-adapter.ts deleted file mode 100644 index 71b2cc1ef..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/page-adapter.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Universal element handle — wraps a single DOM element or Playwright locator. - * Provides a common interface for both jsdom (acceptance-test UI) and Playwright (e2e) contexts. - */ -export interface ElementHandle { - fill(value: string): Promise; - click(): Promise; - check(): Promise; - textContent(): Promise; - getAttribute(name: string): Promise; - isVisible(): Promise; - waitFor(options?: { state?: 'visible' | 'hidden' | 'attached' | 'detached'; timeout?: number }): Promise; - querySelector(selector: string): Promise; - querySelectorAll(selector: string): Promise; -} - -export type PageNavigationWaitUntil = 'load' | 'domcontentloaded' | 'networkidle' | 'commit'; - -export type PageUrlMatcher = string | RegExp | ((url: URL) => boolean); - -/** - * Universal page adapter — abstracts element lookup across jsdom and Playwright. - * Page objects depend on this interface rather than a specific test runner. - */ -export interface PageAdapter { - getByPlaceholder(text: string): ElementHandle; - getByLabel(text: string): ElementHandle; - getByRole(role: string, options?: { name?: string | RegExp }): ElementHandle; - locator(selector: string): ElementHandle; - locatorAll(selector: string): Promise; - getByText(text: string | RegExp, options?: { selector?: string }): ElementHandle; - goto(url: string, options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise; - waitForURL(url: PageUrlMatcher, options?: { timeout?: number; waitUntil?: PageNavigationWaitUntil }): Promise; - url(): string; - waitForTimeout(timeout: number): Promise; -} - -export type PageAdapterMode = 'jsdom' | 'playwright'; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts deleted file mode 100644 index 584459540..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/community.page-interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { CommunityPage } from '../community.page.ts'; - -export type UiCommunityPage = Pick; - -export type E2ECommunityPage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts deleted file mode 100644 index e279f8c47..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/home.page-interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { HomePage } from '../home.page.ts'; - -export type UiHomePage = Pick; - -export type E2EHomePage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts deleted file mode 100644 index 9a095a538..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type { - E2ECommunityPage, - UiCommunityPage, -} from './community.page-interface.ts'; -export type { - E2EHomePage, - UiHomePage, -} from './home.page-interface.ts'; -export type { - E2ELoginPage, - UiLoginPage, -} from './login.page-interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/login.page-interface.ts b/packages/ocom-verification/verification-shared/src/pages/page-interfaces/login.page-interface.ts deleted file mode 100644 index 9792668a5..000000000 --- a/packages/ocom-verification/verification-shared/src/pages/page-interfaces/login.page-interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { LoginPage } from '../login.page.ts'; - -export type UiLoginPage = Pick; - -export type E2ELoginPage = Pick; diff --git a/packages/ocom-verification/verification-shared/src/serenity/index.ts b/packages/ocom-verification/verification-shared/src/serenity/index.ts deleted file mode 100644 index 54ba8c2e5..000000000 --- a/packages/ocom-verification/verification-shared/src/serenity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './task-step.ts'; diff --git a/packages/ocom-verification/verification-shared/src/serenity/task-step.ts b/packages/ocom-verification/verification-shared/src/serenity/task-step.ts deleted file mode 100644 index b11ff0a82..000000000 --- a/packages/ocom-verification/verification-shared/src/serenity/task-step.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Task } from '@serenity-js/core'; - -export class TaskStep extends Task { - constructor( - description: string, - private readonly action: (actor: unknown) => Promise, - ) { - super(description); - } - - performAs(actor: unknown): Promise { - return this.action(actor); - } -} diff --git a/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts b/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts deleted file mode 100644 index acae9ca37..000000000 --- a/packages/ocom-verification/verification-shared/src/servers/graphql-test-server.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { ApolloServer } from '@apollo/server'; -import { startStandaloneServer } from '@apollo/server/standalone'; -import type { ApplicationServices, ApplicationServicesFactory } from '@ocom/application-services'; -import { combinedSchema } from '@ocom/graphql'; -import depthLimit from 'graphql-depth-limit'; -import { applyMiddleware } from 'graphql-middleware'; -import { getTimeout } from '../settings/index.ts'; -import type { TestServer } from './test-server.interface.ts'; - -interface GraphContext { - applicationServices: ApplicationServices; -} - -const MAX_QUERY_DEPTH = 10; - -/** - * In-process Apollo Server for API acceptance and integration tests. - * - * This server runs the GraphQL schema directly in the test process, - * providing fast feedback with mocked application services. - * - * Use this for: - * - API acceptance tests - * - Unit-like integration tests - * - Fast feedback loops - * - * For full system tests, use PortlessServer-based implementations instead. - */ -export class GraphQLTestServer implements TestServer { - private server: ApolloServer | null = null; - private url: string | null = null; - - constructor(private readonly applicationServicesFactory?: ApplicationServicesFactory) {} - - /** - * Start the GraphQL server on the specified port (or random port if 0). - * Uses centralized timeout configuration. - */ - async start(port = 0): Promise { - if (this.server) { - throw new Error('Test server already started'); - } - - const securedSchema = applyMiddleware(combinedSchema); - - this.server = new ApolloServer({ - schema: securedSchema, - allowBatchedHttpRequests: true, - validationRules: [depthLimit(MAX_QUERY_DEPTH)], - introspection: false, - }); - - const timeoutMs = getTimeout('serverStartup'); - const startTime = Date.now(); - - const { url } = await startStandaloneServer(this.server, { - listen: { port }, - context: async ({ req }) => { - const authHeader = req.headers.authorization ?? undefined; - - const applicationServices = this.applicationServicesFactory ? await this.applicationServicesFactory.forRequest(authHeader) : undefined; - - if (!applicationServices) { - throw new Error('ApplicationServicesFactory required for test server'); - } - - return { - applicationServices, - }; - }, - }); - - const elapsed = Date.now() - startTime; - if (elapsed > timeoutMs * 0.8) { - console.warn(`GraphQLTestServer startup took ${elapsed}ms (timeout: ${timeoutMs}ms)`); - } - - this.url = url; - } - - /** - * Stop the server gracefully. - */ - async stop(): Promise { - if (!this.server) { - return; - } - - await this.server.stop(); - this.server = null; - this.url = null; - } - - /** - * Get the server URL. - * @throws Error if server is not running - */ - getUrl(): string { - if (!this.url) { - throw new Error('Test server not started'); - } - return this.url; - } - - /** - * Check if server is currently running. - */ - isRunning(): boolean { - return this.server !== null; - } -} diff --git a/packages/ocom-verification/verification-shared/src/servers/index.ts b/packages/ocom-verification/verification-shared/src/servers/index.ts deleted file mode 100644 index 4f3a5ec63..000000000 --- a/packages/ocom-verification/verification-shared/src/servers/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { seedDatabase } from '../test-data/index.ts'; -export { GraphQLTestServer } from './graphql-test-server.ts'; -export type { - MongoDBSeedDataFunction, - MongoDBTestServerStartOptions, -} from './test-mongodb-server.ts'; -export { MongoDBTestServer } from './test-mongodb-server.ts'; -export type { TestServer } from './test-server.interface.ts'; diff --git a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts b/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts deleted file mode 100644 index 7ae4a488f..000000000 --- a/packages/ocom-verification/verification-shared/src/servers/test-mongodb-server.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { type MongoMemoryReplicaSetConfig, type MongoMemoryReplicaSetDisposer, startMongoMemoryReplicaSet } from '@cellix/server-mongodb-memory-mock-seedwork'; -import { ServiceMongoose } from '@ocom/service-mongoose'; -import { type MongoDBSeedContext, type MongoDBSeedDataFunction, seedDatabase } from '@ocom-verification/verification-shared/test-data'; -import { MongoClient } from 'mongodb'; - -const DEFAULT_DB_NAME = 'owner-community'; -const DEFAULT_MONGO_PORT = 50_000; -const DEFAULT_REPL_SET_NAME = 'globaldb'; - -export type { MongoDBSeedDataFunction }; - -export interface MongoDBTestServerStartOptions { - dbName?: string; - port?: number; - replSetName?: string; - binaryVersion?: string; - attachMongoose?: boolean; - seedData?: MongoDBSeedDataFunction; -} - -/** - * In-memory MongoDB replica set for verification tests. - */ -export class MongoDBTestServer { - private disposer: MongoMemoryReplicaSetDisposer | null = null; - private serviceMongoose: ServiceMongoose | null = null; - private connectionString = ''; - private dbName = DEFAULT_DB_NAME; - private seedData: MongoDBSeedDataFunction = seedDatabase; - - async start(options?: MongoDBTestServerStartOptions): Promise { - const config: MongoMemoryReplicaSetConfig = { - port: options?.port ?? DEFAULT_MONGO_PORT, - dbName: options?.dbName ?? DEFAULT_DB_NAME, - replSetName: options?.replSetName ?? DEFAULT_REPL_SET_NAME, - ...(options?.binaryVersion && { binaryVersion: options.binaryVersion }), - }; - - this.dbName = config.dbName; - this.seedData = options?.seedData ?? seedDatabase; - - const { connectionString, disposer } = await startMongoMemoryReplicaSet(config); - this.disposer = disposer; - this.connectionString = connectionString; - await this.seed(); - - if (options?.attachMongoose) { - await this.attachMongoose(); - } - } - - getServiceMongoose(): ServiceMongoose { - if (!this.serviceMongoose) { - throw new Error('MongoDBTestServer Mongoose service not attached'); - } - return this.serviceMongoose; - } - - getConnectionString(): string { - if (!this.connectionString) { - throw new Error('MongoDBTestServer not started'); - } - return this.connectionString; - } - - async resetForScenario(seedData?: MongoDBSeedDataFunction): Promise { - if (!this.connectionString) { - throw new Error('MongoDBTestServer not started'); - } - - await clearDatabase({ connectionString: this.connectionString, dbName: this.dbName }); - await this.seed(seedData); - } - - async stop(): Promise { - if (this.serviceMongoose) { - await this.serviceMongoose.shutDown(); - this.serviceMongoose = null; - } - if (this.disposer) { - const disposer = this.disposer; - this.disposer = null; - await disposer.stop(); - } - this.connectionString = ''; - } - - isRunning(): boolean { - return this.disposer !== null; - } - - private async attachMongoose(): Promise { - this.serviceMongoose = new ServiceMongoose(this.connectionString, { - dbName: this.dbName, - autoIndex: true, - autoCreate: true, - }); - await this.serviceMongoose.startUp(); - this.clearMongooseModels(); - } - - private clearMongooseModels(): void { - const connection = this.serviceMongoose?.service.connection; - if (!connection) return; - - for (const modelName of Object.keys(connection.models)) { - try { - connection.deleteModel(modelName); - } catch { - /* already deleted */ - } - } - } - - private async seed(seedData = this.seedData): Promise { - await seedData({ connectionString: this.connectionString, dbName: this.dbName }); - } -} - -async function clearDatabase(context: MongoDBSeedContext): Promise { - const client = new MongoClient(context.connectionString); - try { - await client.connect(); - const db = client.db(context.dbName); - const collections = await db.listCollections({}, { nameOnly: true }).toArray(); - await Promise.all(collections.map((collection) => db.collection(collection.name).deleteMany({}))); - } finally { - await client.close(); - } -} diff --git a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts b/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts deleted file mode 100644 index 16e024f23..000000000 --- a/packages/ocom-verification/verification-shared/src/servers/test-server.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Common interface for all test servers (in-process and subprocess). - * Implemented by GraphQLTestServer (in-process), PortlessServer (subprocess - * via the portless proxy), and TestAzuriteServer. - */ -export interface TestServer { - /** Start the server and return when ready */ - start(): Promise; - - /** Stop the server gracefully */ - stop(): Promise; - - /** Check if server is currently running */ - isRunning(): boolean; - - /** Get the server URL (throws if not running) */ - getUrl(): string; -} diff --git a/packages/ocom-verification/verification-shared/src/settings/index.ts b/packages/ocom-verification/verification-shared/src/settings/index.ts deleted file mode 100644 index 5969fa475..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { buildPortlessUrl, getHostnames, PORTLESS_PORT } from './portless-settings.ts'; -export { getTimeout, type TimeoutKey, timeouts } from './timeout-settings.ts'; diff --git a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts b/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts deleted file mode 100644 index 9415b0a64..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/portless-settings.ts +++ /dev/null @@ -1,58 +0,0 @@ -interface PortlessHostnames { - uiCommunity: string; - uiStaff: string; - api: string; - mockAuth: string; - docs: string; -} - -const PORTLESS_PORT = 1355; - -function buildPortlessUrl(hostname: string, path = ''): string { - return `https://${hostname}:${PORTLESS_PORT}${path}`; -} - -function getHostnames(): PortlessHostnames { - const hostnames = { - uiCommunity: requireHostname('VITE_APP_UI_COMMUNITY_BASE_URL'), - uiStaff: requireHostname('VITE_APP_UI_STAFF_AAD_REDIRECT_URI'), - api: requireHostname('VITE_COMMON_API_ENDPOINT'), - mockAuth: requireHostname('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'), - }; - const { WORKTREE_NAME: worktreeName = '' } = process.env; - - return applyWorktreeSuffixes(hostnames, worktreeName); -} - -function hostnameFrom(url: string): string | null { - try { - return new URL(url).hostname; - } catch { - return null; - } -} - -function requireHostname(key: string): string { - const hostname = hostnameFrom(process.env[key] ?? ''); - if (!hostname) { - throw new Error(`portless-settings: required env var ${key} is missing or invalid`); - } - return hostname; -} - -function applyWorktreeSuffixes(hostnames: Omit, worktreeName: string): PortlessHostnames { - return { - uiCommunity: applyWorktreeSuffix(hostnames.uiCommunity, worktreeName), - uiStaff: applyWorktreeSuffix(hostnames.uiStaff, worktreeName), - api: applyWorktreeSuffix(hostnames.api, worktreeName), - mockAuth: applyWorktreeSuffix(hostnames.mockAuth, worktreeName), - docs: applyWorktreeSuffix(`docs.${hostnames.uiCommunity}`, worktreeName), - }; -} - -function applyWorktreeSuffix(hostname: string, worktreeName: string): string { - if (!worktreeName) return hostname; - return hostname.replace('.localhost', `.${worktreeName}.localhost`); -} - -export { buildPortlessUrl, getHostnames, PORTLESS_PORT }; diff --git a/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts b/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts deleted file mode 100644 index 89e55ebaa..000000000 --- a/packages/ocom-verification/verification-shared/src/settings/timeout-settings.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Centralized timeout configuration for all verification test packages. - * - * These timeouts are intentionally generous to accommodate: - * - CI environments with limited resources - * - First-time server startup (cold starts) - * - Parallel test execution contention - */ -export const timeouts = { - /** Default scenario timeout (2 minutes) */ - scenario: 120_000, - - /** Server startup timeout (2 minutes) */ - serverStartup: 120_000, - - /** Server shutdown graceful period (10 seconds) */ - serverShutdown: 10_000, - - /** Health probe timeout (3 seconds) */ - healthProbe: 3_000, - - /** Health probe retry interval (500ms) */ - healthProbeInterval: 500, - - /** UI initialization timeout (30 seconds) */ - uiInit: 30_000, - - /** UI cleanup timeout (10 seconds) */ - uiCleanup: 10_000, -} as const; - -/** Type for timeout configuration keys */ -export type TimeoutKey = keyof typeof timeouts; - -function timeoutEnvName(key: TimeoutKey): string { - return `TIMEOUT_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`; -} - -/** - * Get timeout value with optional override from environment. - * Usage: TIMEOUT_SERVER_STARTUP=300000 npm test - */ -export function getTimeout(key: TimeoutKey): number { - const envName = timeoutEnvName(key); - const envOverride = process.env[envName]; - - if (envOverride) { - const parsed = Number(envOverride); - if (Number.isInteger(parsed) && parsed > 0) { - return parsed; - } - - console.warn(`Ignoring invalid ${envName} value "${envOverride}"; expected a positive integer.`); - } - - return timeouts[key]; -} diff --git a/packages/ocom-verification/verification-shared/src/test-data/seed/end-users.ts b/packages/ocom-verification/verification-shared/src/test-data/seed/end-users.ts index 1141198e8..205524e72 100644 --- a/packages/ocom-verification/verification-shared/src/test-data/seed/end-users.ts +++ b/packages/ocom-verification/verification-shared/src/test-data/seed/end-users.ts @@ -1,5 +1,4 @@ -import type { ActorDetails } from '../../helpers/actor-helpers.ts'; -import { actors } from '../test-actors.ts'; +import { actors, type TestActor } from '../test-actors.ts'; export interface EndUserSeedDocument { _id: string; @@ -31,7 +30,7 @@ export const END_USER_IDS = { export const endUsers: EndUserSeedDocument[] = [createEndUserSeedDocument(END_USER_IDS.communityOwner, actors.CommunityOwner), createEndUserSeedDocument(END_USER_IDS.communityMember, actors.CommunityMember)]; -function createEndUserSeedDocument(id: string, actor: ActorDetails): EndUserSeedDocument { +function createEndUserSeedDocument(id: string, actor: TestActor): EndUserSeedDocument { return { _id: id, userType: 'end-users', diff --git a/packages/ocom-verification/verification-shared/src/test-data/test-actors.ts b/packages/ocom-verification/verification-shared/src/test-data/test-actors.ts index 5c1a7f51d..5a027194a 100644 --- a/packages/ocom-verification/verification-shared/src/test-data/test-actors.ts +++ b/packages/ocom-verification/verification-shared/src/test-data/test-actors.ts @@ -1,6 +1,10 @@ -import type { ActorDetails } from '../helpers/actor-helpers.ts'; - -export type TestActor = ActorDetails; +export interface TestActor { + name: string; + externalId: string; + email: string; + givenName: string; + familyName: string; +} const communityOwner: TestActor = { name: 'CommunityOwner', diff --git a/packages/ocom-verification/verification-shared/src/test-data/utils.ts b/packages/ocom-verification/verification-shared/src/test-data/utils.ts index 6f2bf3a14..79a820f91 100644 --- a/packages/ocom-verification/verification-shared/src/test-data/utils.ts +++ b/packages/ocom-verification/verification-shared/src/test-data/utils.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; +import { ObjectId } from 'mongodb'; /** * Generate a random MongoDB ObjectId string — useful for seeding test data. */ export function generateObjectId(): string { - return new Types.ObjectId().toHexString(); + return new ObjectId().toHexString(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3be5c052c..faafb27b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,8 +49,8 @@ catalogs: specifier: 7.0.0-dev.20260428.1 version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': - specifier: 4.1.2 - version: 4.1.2 + specifier: 4.1.6 + version: 4.1.6 antd: specifier: 6.3.5 version: 6.3.5 @@ -97,10 +97,12 @@ catalogs: specifier: ^0.28.0 version: 0.28.0 vitest: - specifier: 4.1.2 - version: 4.1.2 + specifier: 4.1.6 + version: 4.1.6 overrides: + '@vitest/browser': 4.1.6 + '@vitest/browser-playwright': 4.1.6 axios: 1.16.0 follow-redirects: ^1.16.0 vite: 8.0.5 @@ -157,7 +159,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.2) + version: 6.3.0(vitest@4.1.6) '@ant-design/cli': specifier: ^6.3.5 version: 6.3.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1) @@ -202,7 +204,7 @@ importers: version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) chrome-devtools-mcp: specifier: ^0.21.0 version: 0.21.0 @@ -235,7 +237,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/api: dependencies: @@ -299,7 +301,7 @@ importers: version: link:../../packages/cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) azurite: specifier: ^3.35.0 version: 3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1) @@ -314,7 +316,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/docs: dependencies: @@ -375,7 +377,7 @@ importers: version: 6.0.1(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -384,7 +386,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/server-mongodb-memory-mock: dependencies: @@ -425,7 +427,7 @@ importers: version: link:../../packages/cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -437,7 +439,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-community: dependencies: @@ -507,7 +509,7 @@ importers: version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.16(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.16(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -528,7 +530,7 @@ importers: version: 6.0.1(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) esbuild: specifier: 'catalog:' version: 0.27.4 @@ -558,7 +560,7 @@ importers: version: 0.28.0(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-staff: dependencies: @@ -634,7 +636,7 @@ importers: version: 6.0.1(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) esbuild: specifier: 'catalog:' version: 0.27.4 @@ -658,7 +660,7 @@ importers: version: 0.28.0(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/api-services-spec: devDependencies: @@ -694,7 +696,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-rolldown: devDependencies: @@ -706,7 +708,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -718,7 +720,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-typescript: {} @@ -729,16 +731,16 @@ importers: version: link:../config-typescript '@storybook/addon-vitest': specifier: ^9.1.20 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@vitest/browser-playwright': - specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + specifier: 4.1.6 + version: 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/domain-seedwork: devDependencies: @@ -750,7 +752,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -759,7 +761,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/event-bus-seedwork-node: dependencies: @@ -778,7 +780,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -809,7 +811,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.2) + version: 6.3.0(vitest@4.1.6) '@cellix/config-typescript': specifier: workspace:* version: link:../config-typescript @@ -818,7 +820,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -827,7 +829,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/graphql-core: dependencies: @@ -846,7 +848,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -855,7 +857,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/mongoose-seedwork: dependencies: @@ -874,7 +876,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) mongodb: specifier: 'catalog:' version: 6.18.0 @@ -892,7 +894,74 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + + packages/cellix/serenity-framework: + dependencies: + '@apollo/server': + specifier: 'catalog:' + version: 5.5.0(graphql@16.12.0) + '@cellix/server-mongodb-memory-mock-seedwork': + specifier: workspace:* + version: link:../server-mongodb-memory-mock-seedwork + '@cucumber/cucumber': + specifier: 'catalog:' + version: 12.8.1 + '@cucumber/messages': + specifier: 'catalog:' + version: 32.3.1 + '@serenity-js/core': + specifier: 'catalog:' + version: 3.42.2 + graphql: + specifier: 'catalog:' + version: 16.12.0 + graphql-depth-limit: + specifier: ^1.1.0 + version: 1.1.0(graphql@16.12.0) + jsdom: + specifier: 'catalog:' + version: 26.1.0 + mongodb: + specifier: 'catalog:' + version: 6.18.0 + devDependencies: + '@cellix/config-typescript': + specifier: workspace:* + version: link:../config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../config-vitest + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@types/graphql-depth-limit': + specifier: ^1.1.0 + version: 1.1.6 + '@types/node': + specifier: 'catalog:' + version: 22.19.15 + '@types/react': + specifier: ^19.1.8 + version: 19.2.7 + '@vitest/coverage-istanbul': + specifier: 'catalog:' + version: 4.1.6(vitest@4.1.6) + playwright: + specifier: 1.59.0 + version: 1.59.0 + react: + specifier: ^19.1.0 + version: 19.2.0 + rimraf: + specifier: 'catalog:' + version: 6.0.1 + typescript: + specifier: 'catalog:' + version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/server-mongodb-memory-mock-seedwork: dependencies: @@ -936,7 +1005,7 @@ importers: version: 5.0.5 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -945,7 +1014,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/ui-core: dependencies: @@ -979,7 +1048,7 @@ importers: version: 9.1.16(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -996,14 +1065,14 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + specifier: 4.1.6 + version: 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/browser-playwright': - specifier: ^4.1.2 - version: 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + specifier: 4.1.6 + version: 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -1024,10 +1093,13 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: + '@cellix/serenity-framework': + specifier: workspace:* + version: link:../../cellix/serenity-framework '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 @@ -1046,6 +1118,15 @@ importers: '@serenity-js/serenity-bdd': specifier: 'catalog:' version: 3.42.2 + graphql: + specifier: 'catalog:' + version: 16.12.0 + graphql-depth-limit: + specifier: ^1.1.0 + version: 1.1.0(graphql@16.12.0) + graphql-middleware: + specifier: ^6.1.35 + version: 6.1.35(graphql@16.12.0) std-env: specifier: ^4.0.0 version: 4.0.0 @@ -1062,6 +1143,9 @@ importers: '@ocom/context-spec': specifier: workspace:* version: link:../../ocom/context-spec + '@ocom/graphql': + specifier: workspace:* + version: link:../../ocom/graphql '@ocom/persistence': specifier: workspace:* version: link:../../ocom/persistence @@ -1074,6 +1158,9 @@ importers: '@ocom/service-token-validation': specifier: workspace:* version: link:../../ocom/service-token-validation + '@types/graphql-depth-limit': + specifier: ^1.1.6 + version: 1.1.6 '@types/node': specifier: 'catalog:' version: 22.19.15 @@ -1095,6 +1182,9 @@ importers: '@apollo/client': specifier: ^3.13.9 version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@cellix/serenity-framework': + specifier: workspace:* + version: link:../../cellix/serenity-framework '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 @@ -1153,9 +1243,6 @@ importers: c8: specifier: ^10.1.3 version: 10.1.3 - jsdom: - specifier: ^26.1.0 - version: 26.1.0 tsx: specifier: ^4.20.3 version: 4.21.0 @@ -1182,10 +1269,13 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/e2e-tests: dependencies: + '@cellix/serenity-framework': + specifier: workspace:* + version: link:../../cellix/serenity-framework '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.1 @@ -1232,61 +1322,22 @@ importers: packages/ocom-verification/verification-shared: dependencies: - '@apollo/server': - specifier: 'catalog:' - version: 5.5.0(graphql@16.12.0) - '@cellix/server-mongodb-memory-mock-seedwork': - specifier: workspace:* - version: link:../../cellix/server-mongodb-memory-mock-seedwork - '@cucumber/cucumber': - specifier: 'catalog:' - version: 12.8.1 - '@cucumber/messages': - specifier: 'catalog:' - version: 32.3.1 - '@ocom/application-services': - specifier: workspace:* - version: link:../../ocom/application-services - '@ocom/graphql': - specifier: workspace:* - version: link:../../ocom/graphql - '@ocom/service-mongoose': + '@cellix/serenity-framework': specifier: workspace:* - version: link:../../ocom/service-mongoose + version: link:../../cellix/serenity-framework '@serenity-js/core': specifier: 'catalog:' version: 3.42.2 - '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - graphql: - specifier: 'catalog:' - version: 16.12.0 - graphql-depth-limit: - specifier: ^1.1.0 - version: 1.1.0(graphql@16.12.0) - graphql-middleware: - specifier: ^6.1.35 - version: 6.1.35(graphql@16.12.0) mongodb: specifier: 'catalog:' version: 6.18.0 - mongoose: - specifier: 'catalog:' - version: 8.17.0 devDependencies: '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript - '@types/graphql-depth-limit': - specifier: ^1.1.6 - version: 1.1.6 '@types/node': specifier: 'catalog:' version: 22.19.15 - playwright: - specifier: 1.59.0 - version: 1.59.0 typescript: specifier: 'catalog:' version: 6.0.3 @@ -1317,7 +1368,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1326,7 +1377,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/context-spec: dependencies: @@ -1379,7 +1430,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/domain: dependencies: @@ -1431,7 +1482,7 @@ importers: version: 3.42.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1440,7 +1491,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/event-handler: dependencies: @@ -1493,7 +1544,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1502,7 +1553,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/graphql-handler: dependencies: @@ -1536,7 +1587,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1545,7 +1596,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/persistence: dependencies: @@ -1585,7 +1636,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1594,7 +1645,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/rest: dependencies: @@ -1647,7 +1698,7 @@ importers: version: 1.1.6 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1656,7 +1707,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-blob-storage: dependencies: @@ -1697,7 +1748,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1755,7 +1806,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1764,7 +1815,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-token-validation: dependencies: @@ -1783,7 +1834,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1792,7 +1843,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-accounts: dependencies: @@ -1859,7 +1910,7 @@ importers: version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -1886,7 +1937,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-admin: dependencies: @@ -1953,7 +2004,7 @@ importers: version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -1980,7 +2031,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-root: dependencies: @@ -2023,7 +2074,7 @@ importers: version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -2050,7 +2101,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-shared: dependencies: @@ -2102,7 +2153,7 @@ importers: version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -2129,7 +2180,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-shared: dependencies: @@ -2187,7 +2238,7 @@ importers: version: 9.1.16(@types/react@19.2.7)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) '@storybook/addon-vitest': specifier: ^9.1.3 - version: 9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2) + version: 9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6) '@storybook/react': specifier: ^9.1.9 version: 9.1.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(typescript@6.0.3) @@ -2201,11 +2252,11 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: ^4.1.2 - version: 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + specifier: 4.1.6 + version: 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.2(vitest@4.1.2) + version: 4.1.6(vitest@4.1.6) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -2226,7 +2277,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-community-management: dependencies: @@ -2269,7 +2320,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-finance: dependencies: @@ -2312,7 +2363,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-root: dependencies: @@ -2355,7 +2406,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-tech-admin: dependencies: @@ -2398,7 +2449,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-user-management: dependencies: @@ -2441,7 +2492,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-shared: dependencies: @@ -2490,7 +2541,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -6248,8 +6299,8 @@ packages: '@storybook/addon-vitest@9.1.16': resolution: {integrity: sha512-X0rOOUMb5UHbfekcjnTeiDTarZdsg5irXXPxxL//8QQCFyCLF6Bdm1YNlCdF560PtwaaQPXzlxByD0FfGbtdWA==} peerDependencies: - '@vitest/browser': ^3.0.0 || ^4.0.0 - '@vitest/browser-playwright': ^4.0.0 + '@vitest/browser': 4.1.6 + '@vitest/browser-playwright': 4.1.6 '@vitest/runner': ^3.0.0 || ^4.0.0 storybook: ^9.1.16 vitest: ^3.0.0 || ^4.0.0 @@ -6266,8 +6317,8 @@ packages: '@storybook/addon-vitest@9.1.20': resolution: {integrity: sha512-6zN/qe9Z/7pklbUQJrnjwdTTqRG5PWy4Tyx1J90nwQq6yekj436S8/5NrvXaSns0zYNoZLsynh8nWZPweeu05w==} peerDependencies: - '@vitest/browser': ^3.0.0 || ^4.0.0 - '@vitest/browser-playwright': ^4.0.0 + '@vitest/browser': 4.1.6 + '@vitest/browser-playwright': 4.1.6 '@vitest/runner': ^3.0.0 || ^4.0.0 storybook: ^9.1.20 vitest: ^3.0.0 || ^4.0.0 @@ -6785,27 +6836,27 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/browser-playwright@4.1.2': - resolution: {integrity: sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==} + '@vitest/browser-playwright@4.1.6': + resolution: {integrity: sha512-4csoeyl/qwHyxU2zNL0++WaoDr8YJDXOQPwWPNJoTZ+QzcdO3INYKgF5Zfz730Io7zbkuv914aZmfQ+QE+1Hvw==} peerDependencies: playwright: 1.59.0 - vitest: 4.1.2 + vitest: 4.1.6 - '@vitest/browser@4.1.2': - resolution: {integrity: sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==} + '@vitest/browser@4.1.6': + resolution: {integrity: sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==} peerDependencies: - vitest: 4.1.2 + vitest: 4.1.6 - '@vitest/coverage-istanbul@4.1.2': - resolution: {integrity: sha512-WSz7+4a7PcMtMNvIP7AXUMffsq4JrWeJaguC8lg6fSQyGxSfaT4Rf81idqwxTT6qX5kjjZw2t9rAnCRRQobSqw==} + '@vitest/coverage-istanbul@4.1.6': + resolution: {integrity: sha512-lOt/VDh+sihAx3OUxCE5CC0qZfAhIzE3Dxw75NJ3P0C6ruUgT9b/jZKECE1ctpbxSVic9OkLdXz5UEX39ks4Sw==} peerDependencies: - vitest: 4.1.2 + vitest: 4.1.6 '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.1.2': - resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} @@ -6818,8 +6869,8 @@ packages: vite: optional: true - '@vitest/mocker@4.1.2': - resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} peerDependencies: msw: ^2.4.9 vite: 8.0.5 @@ -6832,26 +6883,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.2': - resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} - '@vitest/runner@4.1.2': - resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} - '@vitest/snapshot@4.1.2': - resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.1.2': - resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.2': - resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -13121,18 +13172,20 @@ packages: yaml: optional: true - vitest@4.1.2: - resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.2 - '@vitest/browser-preview': 4.1.2 - '@vitest/browser-webdriverio': 4.1.2 - '@vitest/ui': 4.1.2 + '@vitest/browser-playwright': 4.1.6 + '@vitest/browser-preview': 4.1.6 + '@vitest/browser-webdriverio': 4.1.6 + '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-v8': 4.1.6 + '@vitest/ui': 4.1.6 happy-dom: '*' jsdom: '*' vite: 8.0.5 @@ -13149,6 +13202,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -13601,13 +13658,13 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.2)': + '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.6)': dependencies: callsites: 4.2.0 minimist: 1.2.8 parsecurrency: 1.1.1 ts-morph: 27.0.2 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@ant-design/cli@6.3.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': dependencies: @@ -15279,7 +15336,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -15315,7 +15372,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -15364,9 +15421,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -18457,7 +18514,7 @@ snapshots: dependencies: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@storybook/addon-vitest@9.1.16(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': + '@storybook/addon-vitest@9.1.16(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -18465,15 +18522,15 @@ snapshots: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/runner': 4.1.2 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/runner': 4.1.6 + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom - '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.2)(@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2))(@vitest/runner@4.1.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.2)': + '@storybook/addon-vitest@9.1.20(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(vitest@4.1.6)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 1.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -18481,10 +18538,10 @@ snapshots: storybook: 9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ts-dedent: 2.2.0 optionalDependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/runner': 4.1.2 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/runner': 4.1.6 + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom @@ -19023,13 +19080,13 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': dependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -19037,29 +19094,29 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': dependencies: - '@vitest/browser': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.2 + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.6 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.1 transitivePeerDependencies: - bufferutil @@ -19068,16 +19125,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2)': + '@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.2 + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.6 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.1 transitivePeerDependencies: - bufferutil @@ -19085,7 +19142,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-istanbul@4.1.2(vitest@4.1.2)': + '@vitest/coverage-istanbul@4.1.6(vitest@4.1.6)': dependencies: '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 @@ -19097,7 +19154,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19109,12 +19166,12 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.1.2': + '@vitest/expect@4.1.6': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 chai: 6.2.2 tinyrainbow: 3.1.0 @@ -19126,17 +19183,17 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.2 + '@vitest/spy': 4.1.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -19146,19 +19203,19 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.2': + '@vitest/pretty-format@4.1.6': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.2': + '@vitest/runner@4.1.6': dependencies: - '@vitest/utils': 4.1.2 + '@vitest/utils': 4.1.6 pathe: 2.0.3 - '@vitest/snapshot@4.1.2': + '@vitest/snapshot@4.1.6': dependencies: - '@vitest/pretty-format': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 magic-string: 0.30.21 pathe: 2.0.3 @@ -19166,7 +19223,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.1.2': {} + '@vitest/spy@4.1.6': {} '@vitest/utils@3.2.4': dependencies: @@ -19174,9 +19231,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.2': + '@vitest/utils@4.1.6': dependencies: - '@vitest/pretty-format': 4.1.2 + '@vitest/pretty-format': 4.1.6 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -26502,15 +26559,15 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -26527,20 +26584,21 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.15 - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) jsdom: 26.1.0 transitivePeerDependencies: - msw - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.2)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.2 - '@vitest/runner': 4.1.2 - '@vitest/snapshot': 4.1.2 - '@vitest/spy': 4.1.2 - '@vitest/utils': 4.1.2 + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -26557,7 +26615,8 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.1.2(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2) + '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) jsdom: 26.1.0 transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9238556fd..9494ac83b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,7 +19,7 @@ catalog: '@serenity-js/cucumber': 3.42.2 '@serenity-js/serenity-bdd': 3.42.2 '@types/node': ^22.19.5 - '@vitest/coverage-istanbul': 4.1.2 + '@vitest/coverage-istanbul': 4.1.6 antd: 6.3.5 archunit: ^2.1.63 esbuild: 0.27.4 @@ -37,7 +37,7 @@ catalog: typescript: 6.0.3 "@typescript/native-preview": 7.0.0-dev.20260428.1 vite: 8.0.5 - vitest: 4.1.2 + vitest: 4.1.6 vite-plugin-node-polyfills: ^0.28.0 auditConfig: @@ -59,6 +59,8 @@ allowBuilds: snyk: true overrides: + '@vitest/browser': 4.1.6 + '@vitest/browser-playwright': 4.1.6 axios: 1.16.0 follow-redirects: ^1.16.0 vite: "catalog:" From 865a5c34300d72e90e5e93210b4ccb4ff0ed27b4 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 2 Jun 2026 15:37:01 -0400 Subject: [PATCH 15/40] reviewed changes on local with opus 4.8, made some adjustments from jsdom to happydom, and overall some code cleanliness changes --- packages/cellix/serenity-framework/README.md | 47 +++-- .../cellix/serenity-framework/manifest.md | 18 +- .../cellix/serenity-framework/package.json | 53 +++--- .../src/clients/graphql-client.ts | 12 +- .../serenity-framework/src/cucumber/hooks.ts | 4 - .../src/{jsdom => dom}/asset-loader-hooks.ts | 4 +- .../src/{jsdom => dom}/css-module-types.d.ts | 0 .../src/{jsdom => dom}/css-modules.ts | 0 .../{jsdom => dom}/register-asset-loader.ts | 2 +- .../src/dom/render-in-dom.ts | 116 +++++++++++++ .../serenity-framework/src/dom/setup.ts | 28 +++ .../src/infrastructure/api.ts | 11 -- .../src/infrastructure/e2e.ts | 9 - .../src/infrastructure/index.ts | 24 ++- .../serenity-framework/src/jsdom/jsdom.d.ts | 6 - .../src/jsdom/react-render.ts | 47 ----- .../serenity-framework/src/jsdom/setup.ts | 160 ------------------ .../{jsdom-adapter.ts => dom-adapter.ts} | 33 ++-- .../src/pages/page-adapter.ts | 2 +- .../src/servers/api-test-server.ts | 13 -- .../src/servers/auth-test-server.ts | 12 -- .../src/servers/azurite-test-server.ts | 13 -- .../serenity-framework/src/servers/index.ts | 10 -- .../src/servers/server-group.test.ts | 45 ----- .../src/servers/test-server-group.ts | 54 ------ .../src/servers/ui-portal-test-server.ts | 13 -- .../src/shared/shared-infrastructure.ts | 4 +- .../acceptance-ui/package.json | 6 +- .../authentication/notes/header-notes.ts | 2 + .../step-definitions/header-login.steps.tsx | 106 ++++-------- .../tasks/click-header-sign-in.ts | 24 +-- .../community/notes/community-notes.ts | 1 - .../questions/community-error-message.ts | 4 - .../create-community.steps.tsx | 110 +++--------- .../community/tasks/create-community.ts | 29 ++-- .../src/shared/cucumber-lifecycle-hooks.ts | 9 +- .../src/step-definitions/index.ts | 1 - .../acceptance-ui/src/world.ts | 58 ++----- .../acceptance-ui/tsconfig.json | 2 +- .../step-definitions/header-login.steps.ts | 2 +- .../src/shared/shared-infrastructure.ts | 14 +- .../src/shared/test-server-factories.ts | 22 +-- pnpm-lock.yaml | 150 ++++++++++------ 43 files changed, 493 insertions(+), 787 deletions(-) delete mode 100644 packages/cellix/serenity-framework/src/cucumber/hooks.ts rename packages/cellix/serenity-framework/src/{jsdom => dom}/asset-loader-hooks.ts (90%) rename packages/cellix/serenity-framework/src/{jsdom => dom}/css-module-types.d.ts (100%) rename packages/cellix/serenity-framework/src/{jsdom => dom}/css-modules.ts (100%) rename packages/cellix/serenity-framework/src/{jsdom => dom}/register-asset-loader.ts (75%) create mode 100644 packages/cellix/serenity-framework/src/dom/render-in-dom.ts create mode 100644 packages/cellix/serenity-framework/src/dom/setup.ts delete mode 100644 packages/cellix/serenity-framework/src/infrastructure/api.ts delete mode 100644 packages/cellix/serenity-framework/src/infrastructure/e2e.ts delete mode 100644 packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts delete mode 100644 packages/cellix/serenity-framework/src/jsdom/react-render.ts delete mode 100644 packages/cellix/serenity-framework/src/jsdom/setup.ts rename packages/cellix/serenity-framework/src/pages/adapters/{jsdom-adapter.ts => dom-adapter.ts} (84%) delete mode 100644 packages/cellix/serenity-framework/src/servers/api-test-server.ts delete mode 100644 packages/cellix/serenity-framework/src/servers/auth-test-server.ts delete mode 100644 packages/cellix/serenity-framework/src/servers/azurite-test-server.ts delete mode 100644 packages/cellix/serenity-framework/src/servers/server-group.test.ts delete mode 100644 packages/cellix/serenity-framework/src/servers/test-server-group.ts delete mode 100644 packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index 8490ef0e5..abacc7c2f 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -6,7 +6,7 @@ This package is intentionally app-agnostic. It provides adapters, a generic Sere ## Page adapters -Page objects should depend on `PageAdapter`, not directly on jsdom or Playwright: +Page objects should depend on `PageAdapter`, not directly on happy-dom or Playwright: ```ts import { AdapterBackedPageObject, type PageAdapter } from '@cellix/serenity-framework/pages'; @@ -26,7 +26,7 @@ class CommunityPage extends AdapterBackedPageObject { Use the runtime-specific adapter at the edge of the test package: ```ts -import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { DomPageAdapter } from '@cellix/serenity-framework/pages/dom'; import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwright'; ``` @@ -36,9 +36,9 @@ Use generic server descriptors for app-specific processes, then load them into t ```ts import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; -import { ApiTestServer, AuthTestServer, AzuriteTestServer, UiPortalTestServer } from '@cellix/serenity-framework/servers'; +import { ProcessTestServer } from '@cellix/serenity-framework/servers'; -const communityPortal = new UiPortalTestServer({ +const communityPortal = new ProcessTestServer({ portalName: 'community', cwd: '/repo/apps/ui-community', getUrl: () => 'https://community.localhost:1355', @@ -51,7 +51,7 @@ export const infrastructure = E2EInfrastructure azuriteServer, authServer, createApiServer: ({ getMongoConnectionString }) => - new ApiTestServer({ + new ProcessTestServer({ serverName: 'Api', executable: 'pnpm', spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], @@ -115,17 +115,40 @@ export const ApiWorld = registerManagedSerenityWorld({ }); ``` -## jsdom helpers +## DOM (happy-dom) helpers -Component-level acceptance tests can import framework jsdom setup and asset-loader hooks instead of carrying per-suite copies: +Component-level acceptance tests run against an in-process DOM provided by +happy-dom. Preload the DOM setup and asset-loader hooks before any module that +imports `react-dom`, so React binds its event system to the happy-dom +environment. Both run as Node `--import` preloads, which is order-independent: ```sh -NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js +NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' cucumber-js ``` +Include `@cellix/serenity-framework/src/dom/css-module-types.d.ts` in tsconfig +when component imports include CSS modules. + +## Rendering components through actors + +Give actors the `RenderInDom` ability and let page objects read their root +element from the actor, instead of threading a container through world state or +task parameters. This is the in-process DOM counterpart to a browser +`BrowseTheWeb` ability, so component acceptance tests and browser E2E tests share +the same actor-centric shape. The ability unmounts the rendered tree when the +scenario ends. + ```ts -import '@cellix/serenity-framework/jsdom/setup'; -/// Include `@cellix/serenity-framework/src/jsdom/css-module-types.d.ts` -/// in tsconfig when component imports include CSS modules. -import { mountComponent, unmountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +import { Render, RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; +import { DomPageAdapter } from '@cellix/serenity-framework/pages/dom'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; + +// cast: grant every actor the ability +new SerenityCast({ useNotepad: true, abilities: [() => new RenderInDom()] }); + +// Given: render through the actor +await actor.attemptsTo(Render.component(, { wrapper: withProviders() })); + +// task/question: build the page object from the actor's container +const page = new LoginPage(new DomPageAdapter(RenderInDom.as(actor).container)); ``` diff --git a/packages/cellix/serenity-framework/manifest.md b/packages/cellix/serenity-framework/manifest.md index 11d6563c4..0e2f58705 100644 --- a/packages/cellix/serenity-framework/manifest.md +++ b/packages/cellix/serenity-framework/manifest.md @@ -8,13 +8,13 @@ Provide reusable Serenity/JS, Cucumber, page-adapter, and test-server framework This package owns generic verification infrastructure only: -- Serenity task, cast, and browser-ability primitives +- Serenity task, cast, browser-ability, and in-process DOM render-ability primitives - Cucumber data-table, lifecycle, screenshot, and managed-world helpers -- Runtime-agnostic page adapter contracts and jsdom/Playwright adapter implementations -- jsdom globals, CSS module declarations, asset-loader hooks, and generic React render helpers for component acceptance tests +- Runtime-agnostic page adapter contracts and in-process DOM (happy-dom) / Playwright adapter implementations +- DOM globals (via happy-dom), CSS module declarations, asset-loader hooks, and generic React render helpers for component acceptance tests - Adapter-backed page-object base contracts - Timeout utilities -- Configurable process, UI portal, Apollo GraphQL, Azurite, Mongo memory, and server-group lifecycle utilities +- Configurable process, Apollo GraphQL, and Mongo memory server lifecycle utilities - API acceptance and browser E2E infrastructure managers that create MongoDB from options and compose consumer-owned server factories ## Non-goals @@ -29,13 +29,13 @@ This package owns generic verification infrastructure only: - `@cellix/serenity-framework/cucumber`: `ActorName`, `GherkinDataTable`, lifecycle hook helpers - `@cellix/serenity-framework/cucumber/screenshot`: browser screenshot-on-failure hook helpers - `@cellix/serenity-framework/pages`: adapter contracts and page-object base types -- `@cellix/serenity-framework/pages/jsdom`: `JsdomPageAdapter` +- `@cellix/serenity-framework/pages/dom`: `DomPageAdapter` - `@cellix/serenity-framework/pages/playwright`: `PlaywrightPageAdapter` - `@cellix/serenity-framework/clients/graphql`: `GraphQLClient` -- `@cellix/serenity-framework/jsdom/setup`: jsdom global bootstrap side-effect module -- `@cellix/serenity-framework/jsdom/register-asset-loader`: jsdom asset-loader registration side-effect module -- `@cellix/serenity-framework/jsdom/react-render`: generic React mount/unmount helpers -- `@cellix/serenity-framework/jsdom/css-modules`: package-owned CSS module declaration target +- `@cellix/serenity-framework/dom/setup`: DOM global bootstrap side-effect module (happy-dom) +- `@cellix/serenity-framework/dom/register-asset-loader`: asset-loader registration side-effect module +- `@cellix/serenity-framework/dom/render-in-dom`: `RenderInDom` ability and `Render` interaction for rendering components through actors +- `@cellix/serenity-framework/dom/css-modules`: package-owned CSS module declaration target - `@cellix/serenity-framework/serenity`: `TaskStep`, `SerenityCast` - `@cellix/serenity-framework/serenity/browser`: `BrowseTheWeb` - `@cellix/serenity-framework/infrastructure/api`: API acceptance infrastructure manager with MongoDB options, optional Mongoose service management, and an API server factory diff --git a/packages/cellix/serenity-framework/package.json b/packages/cellix/serenity-framework/package.json index fe50b41f5..50f49683c 100644 --- a/packages/cellix/serenity-framework/package.json +++ b/packages/cellix/serenity-framework/package.json @@ -7,7 +7,7 @@ "types": "dist/index.d.ts", "files": [ "dist", - "src/jsdom/css-module-types.d.ts" + "src/dom/css-module-types.d.ts" ], "exports": { ".": { @@ -30,10 +30,6 @@ "types": "./dist/cucumber/gherkin-data-table.d.ts", "default": "./dist/cucumber/gherkin-data-table.js" }, - "./cucumber/hooks": { - "types": "./dist/cucumber/hooks.d.ts", - "default": "./dist/cucumber/hooks.js" - }, "./cucumber/screenshot": { "types": "./dist/cucumber/screenshot-hooks.d.ts", "default": "./dist/cucumber/screenshot-hooks.js" @@ -47,44 +43,44 @@ "default": "./dist/infrastructure/index.js" }, "./infrastructure/api": { - "types": "./dist/infrastructure/api.d.ts", - "default": "./dist/infrastructure/api.js" + "types": "./dist/infrastructure/api-infrastructure.d.ts", + "default": "./dist/infrastructure/api-infrastructure.js" }, "./infrastructure/e2e": { - "types": "./dist/infrastructure/e2e.d.ts", - "default": "./dist/infrastructure/e2e.js" + "types": "./dist/infrastructure/e2e-infrastructure.d.ts", + "default": "./dist/infrastructure/e2e-infrastructure.js" }, "./pages": { "types": "./dist/pages/index.d.ts", "default": "./dist/pages/index.js" }, - "./pages/jsdom": { - "types": "./dist/pages/adapters/jsdom-adapter.d.ts", - "default": "./dist/pages/adapters/jsdom-adapter.js" + "./pages/dom": { + "types": "./dist/pages/adapters/dom-adapter.d.ts", + "default": "./dist/pages/adapters/dom-adapter.js" }, "./pages/playwright": { "types": "./dist/pages/adapters/playwright-adapter.d.ts", "default": "./dist/pages/adapters/playwright-adapter.js" }, - "./jsdom/setup": { - "types": "./dist/jsdom/setup.d.ts", - "default": "./dist/jsdom/setup.js" + "./dom/setup": { + "types": "./dist/dom/setup.d.ts", + "default": "./dist/dom/setup.js" }, - "./jsdom/register-asset-loader": { - "types": "./dist/jsdom/register-asset-loader.d.ts", - "default": "./dist/jsdom/register-asset-loader.js" + "./dom/register-asset-loader": { + "types": "./dist/dom/register-asset-loader.d.ts", + "default": "./dist/dom/register-asset-loader.js" }, - "./jsdom/asset-loader-hooks": { - "types": "./dist/jsdom/asset-loader-hooks.d.ts", - "default": "./dist/jsdom/asset-loader-hooks.js" + "./dom/asset-loader-hooks": { + "types": "./dist/dom/asset-loader-hooks.d.ts", + "default": "./dist/dom/asset-loader-hooks.js" }, - "./jsdom/react-render": { - "types": "./dist/jsdom/react-render.d.ts", - "default": "./dist/jsdom/react-render.js" + "./dom/render-in-dom": { + "types": "./dist/dom/render-in-dom.d.ts", + "default": "./dist/dom/render-in-dom.js" }, - "./jsdom/css-modules": { - "types": "./src/jsdom/css-module-types.d.ts", - "default": "./dist/jsdom/css-modules.js" + "./dom/css-modules": { + "types": "./src/dom/css-module-types.d.ts", + "default": "./dist/dom/css-modules.js" }, "./serenity": { "types": "./dist/serenity/index.d.ts", @@ -119,10 +115,11 @@ "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", "@cucumber/cucumber": "catalog:", "@cucumber/messages": "catalog:", + "@happy-dom/global-registrator": "^20.9.0", "@serenity-js/core": "catalog:", "graphql": "catalog:", "graphql-depth-limit": "^1.1.0", - "jsdom": "catalog:", + "happy-dom": "^20.9.0", "mongodb": "catalog:" }, "peerDependencies": { diff --git a/packages/cellix/serenity-framework/src/clients/graphql-client.ts b/packages/cellix/serenity-framework/src/clients/graphql-client.ts index 49d851451..7aaa5076f 100644 --- a/packages/cellix/serenity-framework/src/clients/graphql-client.ts +++ b/packages/cellix/serenity-framework/src/clients/graphql-client.ts @@ -76,14 +76,22 @@ export class GraphQLClient extends Ability { body: JSON.stringify({ query, variables }), }); - const result = (await response.json()) as GraphQLResponse; + let result: GraphQLResponse; + try { + result = (await response.json()) as GraphQLResponse; + } catch (parseError) { + if (!response.ok) { + throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`); + } + throw parseError; + } if (result.errors?.length) { throw new Error(result.errors.map((error) => error.message ?? 'Unknown error').join('; ')); } if (!response.ok) { - throw new Error(`GraphQL error: ${response.status} ${response.statusText}`); + throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`); } return result; diff --git a/packages/cellix/serenity-framework/src/cucumber/hooks.ts b/packages/cellix/serenity-framework/src/cucumber/hooks.ts deleted file mode 100644 index bcf61416b..000000000 --- a/packages/cellix/serenity-framework/src/cucumber/hooks.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type { WorldLifecycleHooks } from './lifecycle-hooks.ts'; -export { registerWorldLifecycleHooks } from './lifecycle-hooks.ts'; -export type { ScreenshotOnFailureOptions } from './screenshot-hooks.ts'; -export { registerScreenshotOnFailureHook } from './screenshot-hooks.ts'; diff --git a/packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts b/packages/cellix/serenity-framework/src/dom/asset-loader-hooks.ts similarity index 90% rename from packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts rename to packages/cellix/serenity-framework/src/dom/asset-loader-hooks.ts index eab193590..f6652fb22 100644 --- a/packages/cellix/serenity-framework/src/jsdom/asset-loader-hooks.ts +++ b/packages/cellix/serenity-framework/src/dom/asset-loader-hooks.ts @@ -2,7 +2,7 @@ * ESM loader hooks that intercept CSS, image, and other non-JS imports so * they resolve to empty modules instead of throwing in Node.js. * - * Usage: `NODE_OPTIONS='--import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js` + * Usage: `NODE_OPTIONS='--import @cellix/serenity-framework/dom/register-asset-loader' cucumber-js` */ const ASSET_RE = /\.(css|less|scss|sass|svg|png|jpe?g|gif|webp|woff2?|ttf|eot|ico)$/i; @@ -26,7 +26,7 @@ export interface AssetLoaderResolveResult { export type NextAssetLoaderResolve = (specifier: string, context: AssetLoaderResolveContext) => Promise; /** - * Resolve CSS, image, font, and Ant Design ESM imports for jsdom acceptance tests. + * Resolve CSS, image, font, and Ant Design ESM imports for component acceptance tests. * * Asset imports resolve to empty JavaScript modules. Ant Design `antd/es/*` * imports are redirected to `antd/lib/*` when possible because many Node-based diff --git a/packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts b/packages/cellix/serenity-framework/src/dom/css-module-types.d.ts similarity index 100% rename from packages/cellix/serenity-framework/src/jsdom/css-module-types.d.ts rename to packages/cellix/serenity-framework/src/dom/css-module-types.d.ts diff --git a/packages/cellix/serenity-framework/src/jsdom/css-modules.ts b/packages/cellix/serenity-framework/src/dom/css-modules.ts similarity index 100% rename from packages/cellix/serenity-framework/src/jsdom/css-modules.ts rename to packages/cellix/serenity-framework/src/dom/css-modules.ts diff --git a/packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts b/packages/cellix/serenity-framework/src/dom/register-asset-loader.ts similarity index 75% rename from packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts rename to packages/cellix/serenity-framework/src/dom/register-asset-loader.ts index d13d50cd8..9601fa9eb 100644 --- a/packages/cellix/serenity-framework/src/jsdom/register-asset-loader.ts +++ b/packages/cellix/serenity-framework/src/dom/register-asset-loader.ts @@ -3,7 +3,7 @@ * without errors in Node.js. * * Use via NODE_OPTIONS: - * `NODE_OPTIONS='--import @cellix/serenity-framework/jsdom/register-asset-loader'`. + * `NODE_OPTIONS='--import @cellix/serenity-framework/dom/register-asset-loader'`. */ import { register } from 'node:module'; diff --git a/packages/cellix/serenity-framework/src/dom/render-in-dom.ts b/packages/cellix/serenity-framework/src/dom/render-in-dom.ts new file mode 100644 index 000000000..066c7acc6 --- /dev/null +++ b/packages/cellix/serenity-framework/src/dom/render-in-dom.ts @@ -0,0 +1,116 @@ +import { Ability, type Discardable, Interaction } from '@serenity-js/core'; +import { type RenderResult, render } from '@testing-library/react'; +import type { ReactElement } from 'react'; + +/** Wraps a rendered React element before it is mounted (providers, routing, theme). */ +export type ReactRenderWrapper = (children: ReactElement) => ReactElement; + +/** Options accepted when rendering a component through {@link RenderInDom}. */ +export interface RenderComponentOptions { + /** Optional wrapper supplying providers such as routing, theme, or GraphQL. */ + wrapper?: ReactRenderWrapper; +} + +/** + * Serenity ability that renders React components into the active in-process DOM + * and owns the resulting container for the lifetime of a scenario. + * + * This is the in-process DOM counterpart to a browser `BrowseTheWeb` ability: + * page objects obtain their root element from the actor's ability rather than + * receiving a container through world state or task parameters, so component + * acceptance tests and browser E2E tests share the same actor-centric shape. + * + * The ability is {@link Discardable}; Serenity unmounts the rendered tree when + * the actor is dismissed. Suites that do not rely on Serenity actor dismissal + * can also call {@link unmount} from a test-runner teardown hook. + * + * @example + * ```ts + * const cast = new SerenityCast({ useNotepad: true, abilities: [() => new RenderInDom()] }); + * + * // in a step: + * await actor.attemptsTo(Render.component(, { wrapper: withProviders() })); + * const page = new LoginPage(new DomPageAdapter(RenderInDom.as(actor).container)); + * ``` + */ +export class RenderInDom extends Ability implements Discardable { + private rendered: RenderResult | undefined; + + /** + * Create a render ability with no component mounted yet. + * + * Declared explicitly to widen Serenity's `protected` ability constructor to + * `public`, so the cast can instantiate it and `RenderInDom.as(actor)` resolves. + */ + // biome-ignore lint/complexity/noUselessConstructor: widens the inherited protected constructor to public. + constructor() { + super(); + } + + /** + * Render a React element, unmounting any element previously rendered by this + * ability so scenarios do not leak DOM state. + * + * @param ui React element to render. + * @param options Optional provider wrapper. + * @returns Testing Library render result for the mounted component. + */ + render(ui: ReactElement, options?: RenderComponentOptions): RenderResult { + this.unmount(); + this.rendered = render(options?.wrapper ? options.wrapper(ui) : ui); + return this.rendered; + } + + /** + * Root element that scopes all page-object selections for the current render. + * + * @throws Error when no component has been rendered yet. + */ + get container(): HTMLElement { + return this.currentResult().container; + } + + /** + * Testing Library render result for the current render. + * + * @throws Error when no component has been rendered yet. + */ + get result(): RenderResult { + return this.currentResult(); + } + + /** Unmount the current render, if one exists. */ + unmount(): void { + this.rendered?.unmount(); + this.rendered = undefined; + } + + /** Unmount on actor dismissal. Invoked by Serenity when the scene finishes. */ + discard(): void { + this.unmount(); + } + + private currentResult(): RenderResult { + if (!this.rendered) { + throw new Error('RenderInDom: no component has been rendered — did the Given step run?'); + } + return this.rendered; + } +} + +/** + * Screenplay interactions for rendering components through {@link RenderInDom}. + */ +export const Render = { + /** + * Render a React component into the active DOM via the actor's + * {@link RenderInDom} ability. + * + * @param ui React element to render. + * @param options Optional provider wrapper. + */ + component: (ui: ReactElement, options?: RenderComponentOptions): Interaction => + Interaction.where('#actor renders a component', (actor) => { + RenderInDom.as(actor).render(ui, options); + }), +} as const; diff --git a/packages/cellix/serenity-framework/src/dom/setup.ts b/packages/cellix/serenity-framework/src/dom/setup.ts new file mode 100644 index 000000000..a88bc972c --- /dev/null +++ b/packages/cellix/serenity-framework/src/dom/setup.ts @@ -0,0 +1,28 @@ +/** + * DOM environment bootstrap for component-level acceptance tests. + * + * Registers a complete set of browser globals — `window`, `document`, + * `navigator`, the DOM constructor classes, and modern layout APIs such as + * `matchMedia`, `ResizeObserver`, and `IntersectionObserver` — onto the Node + * global object via + * {@link https://github.com/capricorn86/happy-dom | happy-dom}'s global + * registrator. + * + * happy-dom implements the layout/visual APIs that Ant Design and React Router + * touch at import or render time, so unlike jsdom this setup needs no manual + * polyfills. + * + * Load this module for its side effects before any module that imports + * `react-dom`, so React binds its event system to the happy-dom environment. + * Prefer a Node `--import` preload, which is order-independent: + * + * ```sh + * NODE_OPTIONS='--import @cellix/serenity-framework/dom/setup' cucumber-js + * ``` + * + * @packageDocumentation + */ + +import { GlobalRegistrator } from '@happy-dom/global-registrator'; + +GlobalRegistrator.register({ url: 'http://localhost:3000' }); diff --git a/packages/cellix/serenity-framework/src/infrastructure/api.ts b/packages/cellix/serenity-framework/src/infrastructure/api.ts deleted file mode 100644 index d4cc0bb9b..000000000 --- a/packages/cellix/serenity-framework/src/infrastructure/api.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type { - ApiInfrastructureMongooseOptions, - ApiInfrastructureOptions, - ApiInfrastructureState, - ApiMongoServerFactory, - ApiServerFactory, - ApiServerFactoryContext, - ManagedMongooseConnection, - ManagedMongooseService, -} from './api-infrastructure.ts'; -export { ApiInfrastructure } from './api-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e.ts deleted file mode 100644 index 3e93f3921..000000000 --- a/packages/cellix/serenity-framework/src/infrastructure/e2e.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type { - E2EApiServerFactory, - E2EInfrastructureCallbackContext, - E2EInfrastructureOptions, - E2EInfrastructureState, - E2EMongoServerFactory, - UiPortalRegistration, -} from './e2e-infrastructure.ts'; -export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/index.ts b/packages/cellix/serenity-framework/src/infrastructure/index.ts index 5c4ee90c3..5334f34c0 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/index.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/index.ts @@ -1,4 +1,20 @@ -export type { ApiInfrastructureOptions, ApiInfrastructureState } from './api.ts'; -export { ApiInfrastructure } from './api.ts'; -export type { E2EInfrastructureCallbackContext, E2EInfrastructureOptions, E2EInfrastructureState, UiPortalRegistration } from './e2e.ts'; -export { E2EInfrastructure } from './e2e.ts'; +export type { + ApiInfrastructureMongooseOptions, + ApiInfrastructureOptions, + ApiInfrastructureState, + ApiMongoServerFactory, + ApiServerFactory, + ApiServerFactoryContext, + ManagedMongooseConnection, + ManagedMongooseService, +} from './api-infrastructure.ts'; +export { ApiInfrastructure } from './api-infrastructure.ts'; +export type { + E2EApiServerFactory, + E2EInfrastructureCallbackContext, + E2EInfrastructureOptions, + E2EInfrastructureState, + E2EMongoServerFactory, + UiPortalRegistration, +} from './e2e-infrastructure.ts'; +export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts b/packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts deleted file mode 100644 index b8f49557a..000000000 --- a/packages/cellix/serenity-framework/src/jsdom/jsdom.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module 'jsdom' { - export class JSDOM { - constructor(html?: string, options?: Record); - readonly [key: string]: Window & typeof globalThis; - } -} diff --git a/packages/cellix/serenity-framework/src/jsdom/react-render.ts b/packages/cellix/serenity-framework/src/jsdom/react-render.ts deleted file mode 100644 index fb1065fe2..000000000 --- a/packages/cellix/serenity-framework/src/jsdom/react-render.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { type RenderResult, render } from '@testing-library/react'; -import type React from 'react'; - -/** Wraps a rendered React element before it is mounted. */ -export type ReactRenderWrapper = (children: React.ReactElement) => React.ReactElement; - -/** Options used by {@link mountComponent}. */ -export interface ReactMountOptions { - /** Optional wrapper used for providers such as routing, theme, or GraphQL. */ - wrapper?: ReactRenderWrapper; -} - -let rendered: RenderResult | null = null; - -/** - * Mount a React element into the active jsdom document. - * - * Any previously mounted element is unmounted first so component-level - * acceptance tests do not leak state between scenarios. - * - * @param ui React element to mount. - * @param options Optional provider wrapper. - * @returns Testing Library render result for the mounted component. - */ -export function mountComponent(ui: React.ReactElement, options?: ReactMountOptions): RenderResult { - unmountComponent(); - - rendered = render(options?.wrapper ? options.wrapper(ui) : ui); - return rendered; -} - -/** - * Unmount the currently mounted component, when one exists. - */ -export function unmountComponent(): void { - if (rendered) { - rendered.unmount(); - rendered = null; - } -} - -/** - * Return the current Testing Library render result. - */ -export function getRendered(): RenderResult | null { - return rendered; -} diff --git a/packages/cellix/serenity-framework/src/jsdom/setup.ts b/packages/cellix/serenity-framework/src/jsdom/setup.ts deleted file mode 100644 index 6b7116689..000000000 --- a/packages/cellix/serenity-framework/src/jsdom/setup.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * JSDOM environment setup — initialises global browser APIs that libraries like - * antd or React Router rely on at import time. - * - * Must be imported before any React component code runs. - */ - -import { JSDOM } from 'jsdom'; - -const dom = new JSDOM('
', { - url: 'http://localhost:3000', - pretendToBeVisual: true, -}); -// biome-ignore lint/complexity/useLiteralKeys: `dom.window` is exposed via JSDOM's index signature, requiring bracket access under strict TypeScript -const domGlobal = dom['window'] as unknown as Window & typeof globalThis; - -// biome-ignore lint/suspicious/noExplicitAny: attaching browser globals requires dynamic property assignment -const g = globalThis as any; - -/** - * Safely assign a global — falls back to Object.defineProperty when the - * property is read-only (e.g. `navigator` in Node 22). - */ -const safeAssign = (name: string, value: unknown) => { - try { - g[name] = value; - } catch { - Object.defineProperty(globalThis, name, { - value, - writable: true, - configurable: true, - }); - } -}; - -safeAssign('window', domGlobal); -safeAssign('document', domGlobal.document); -safeAssign('navigator', domGlobal.navigator); -safeAssign('HTMLElement', domGlobal.HTMLElement); -safeAssign('HTMLInputElement', domGlobal.HTMLInputElement); -safeAssign('HTMLTextAreaElement', domGlobal.HTMLTextAreaElement); -safeAssign('HTMLFormElement', domGlobal.HTMLFormElement); -safeAssign('HTMLButtonElement', domGlobal.HTMLButtonElement); -safeAssign('HTMLSelectElement', domGlobal.HTMLSelectElement); -safeAssign('HTMLAnchorElement', domGlobal.HTMLAnchorElement); -safeAssign('Element', domGlobal.Element); -safeAssign('SVGElement', domGlobal.SVGElement); -safeAssign('ShadowRoot', domGlobal.ShadowRoot ?? class ShadowRoot {}); -safeAssign('Node', domGlobal.Node); -safeAssign('NodeList', domGlobal.NodeList); -safeAssign('Event', domGlobal.Event); -safeAssign('CustomEvent', domGlobal.CustomEvent); -safeAssign('KeyboardEvent', domGlobal.KeyboardEvent); -safeAssign('MouseEvent', domGlobal.MouseEvent); -safeAssign('getComputedStyle', domGlobal.getComputedStyle); -safeAssign('requestAnimationFrame', (cb: () => void) => setTimeout(cb, 0)); -safeAssign('cancelAnimationFrame', (id: number) => clearTimeout(id)); -safeAssign('location', domGlobal.location); -safeAssign('history', domGlobal.history); -safeAssign('MutationObserver', domGlobal.MutationObserver); -safeAssign('URL', domGlobal.URL); -safeAssign('URLSearchParams', domGlobal.URLSearchParams); -safeAssign('SubmitEvent', domGlobal.SubmitEvent); - -/* --- Stubs for APIs not supported by jsdom --- */ - -domGlobal.matchMedia = - domGlobal.matchMedia || - (() => ({ - matches: false, - addListener: () => { - /* noop stub */ - }, - removeListener: () => { - /* noop stub */ - }, - addEventListener: () => { - /* noop stub */ - }, - removeEventListener: () => { - /* noop stub */ - }, - dispatchEvent: () => false, - media: '', - onchange: null, - })); - -g.ResizeObserver = - g.ResizeObserver || - class { - observe() { - /* noop stub */ - } - unobserve() { - /* noop stub */ - } - disconnect() { - /* noop stub */ - } - }; - -g.IntersectionObserver = - g.IntersectionObserver || - class { - observe() { - /* noop stub */ - } - unobserve() { - /* noop stub */ - } - disconnect() { - /* noop stub */ - } - }; - -domGlobal.scrollTo = - domGlobal.scrollTo || - (() => { - /* noop stub */ - }); -domGlobal.scroll = - domGlobal.scroll || - (() => { - /* noop stub */ - }); -domGlobal.resizeTo = - domGlobal.resizeTo || - (() => { - /* noop stub */ - }); - -domGlobal.getComputedStyle = - domGlobal.getComputedStyle || - (() => ({ - getPropertyValue: () => '', - })); - -g.document.elementFromPoint = g.document.elementFromPoint || (() => null); -g.document.elementsFromPoint = g.document.elementsFromPoint || (() => []); - -// jsdom does not implement form.requestSubmit(), but clicking a submit button -// uses it internally. Dispatch a cancelable submit event so form handlers can -// drive test state the same way a browser-backed form flow would. -g.HTMLFormElement.prototype.requestSubmit = function requestSubmit(submitter?: HTMLElement) { - if (typeof this.checkValidity === 'function' && !this.checkValidity()) { - return; - } - - const submitEvent = new g.Event('submit', { - bubbles: true, - cancelable: true, - }); - - Object.defineProperty(submitEvent, 'submitter', { - value: submitter ?? null, - configurable: true, - }); - - this.dispatchEvent(submitEvent); -}; diff --git a/packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts b/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts similarity index 84% rename from packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts rename to packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts index c82c7c412..8ce76571a 100644 --- a/packages/cellix/serenity-framework/src/pages/adapters/jsdom-adapter.ts +++ b/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts @@ -25,9 +25,9 @@ function findLabelControl(container: Element, text: string): Element | null { } /** - * Element handle backed by a jsdom `Element`. + * Element handle backed by an in-process DOM `Element` (happy-dom or jsdom). */ -export class JsdomElementHandle implements ElementHandle { +export class DomElementHandle implements ElementHandle { /** * @param element Element to adapt, or `null` for a missing selection. */ @@ -97,35 +97,36 @@ export class JsdomElementHandle implements ElementHandle { querySelector(selector: string): Promise { const child = this.element?.querySelector(selector) ?? null; - return Promise.resolve(child ? new JsdomElementHandle(child) : null); + return Promise.resolve(child ? new DomElementHandle(child) : null); } querySelectorAll(selector: string): Promise { if (!this.element) { return Promise.resolve([]); } - return Promise.resolve(Array.from(this.element.querySelectorAll(selector)).map((element) => new JsdomElementHandle(element))); + return Promise.resolve(Array.from(this.element.querySelectorAll(selector)).map((element) => new DomElementHandle(element))); } } /** - * Page adapter backed by a jsdom container element. + * Page adapter backed by an in-process DOM container element. * - * Use this adapter in component-level Cucumber tests that render React into - * jsdom while reusing the same page-object classes used by browser E2E tests. + * Use this adapter in component-level Cucumber tests that render React into an + * in-process DOM (happy-dom or jsdom) while reusing the same page-object + * classes used by browser E2E tests. */ -export class JsdomPageAdapter implements PageAdapter { +export class DomPageAdapter implements PageAdapter { /** * @param container Root element that scopes all selections for this page. */ constructor(private readonly container: Element) {} getByPlaceholder(text: string): ElementHandle { - return new JsdomElementHandle(this.container.querySelector(`[placeholder="${text}"], [placeholder*="${text}"]`)); + return new DomElementHandle(this.container.querySelector(`[placeholder="${text}"], [placeholder*="${text}"]`)); } getByLabel(text: string): ElementHandle { - return new JsdomElementHandle(findLabelControl(this.container, text)); + return new DomElementHandle(findLabelControl(this.container, text)); } getByRole(role: string, options?: { name?: string | RegExp }): ElementHandle { @@ -154,18 +155,18 @@ export class JsdomPageAdapter implements PageAdapter { const ariaLabel = element.getAttribute('aria-label') ?? ''; return nameFilter instanceof RegExp ? nameFilter.test(textContent) || nameFilter.test(ariaLabel) : textContent.includes(nameFilter) || ariaLabel.includes(nameFilter); }); - return new JsdomElementHandle(match ?? null); + return new DomElementHandle(match ?? null); } - return new JsdomElementHandle(candidates[0] ?? null); + return new DomElementHandle(candidates[0] ?? null); } locator(selector: string): ElementHandle { - return new JsdomElementHandle(this.container.querySelector(selector)); + return new DomElementHandle(this.container.querySelector(selector)); } locatorAll(selector: string): Promise { - return Promise.resolve(Array.from(this.container.querySelectorAll(selector)).map((element) => new JsdomElementHandle(element))); + return Promise.resolve(Array.from(this.container.querySelectorAll(selector)).map((element) => new DomElementHandle(element))); } getByText(text: string | RegExp, options?: { selector?: string }): ElementHandle { @@ -177,10 +178,10 @@ export class JsdomPageAdapter implements PageAdapter { const content = node.textContent ?? ''; const matches = text instanceof RegExp ? text.test(content) : content.includes(text); if (matches && node.parentElement) { - return new JsdomElementHandle(node.parentElement); + return new DomElementHandle(node.parentElement); } } - return new JsdomElementHandle(null); + return new DomElementHandle(null); } goto(url: string, _options?: PageNavigationOptions): Promise { diff --git a/packages/cellix/serenity-framework/src/pages/page-adapter.ts b/packages/cellix/serenity-framework/src/pages/page-adapter.ts index 0889f07fb..de24f5a37 100644 --- a/packages/cellix/serenity-framework/src/pages/page-adapter.ts +++ b/packages/cellix/serenity-framework/src/pages/page-adapter.ts @@ -100,4 +100,4 @@ export interface PageAdapter { } /** Supported adapter runtime labels. */ -export type PageAdapterMode = 'jsdom' | 'playwright'; +export type PageAdapterMode = 'dom' | 'playwright'; diff --git a/packages/cellix/serenity-framework/src/servers/api-test-server.ts b/packages/cellix/serenity-framework/src/servers/api-test-server.ts deleted file mode 100644 index 09b927560..000000000 --- a/packages/cellix/serenity-framework/src/servers/api-test-server.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; - -/** Options used by {@link ApiTestServer}. */ -export type ApiTestServerOptions = ProcessTestServerOptions; - -/** - * Process-backed API server for verification suites. - * - * The framework supplies lifecycle behavior only. Consumers provide every - * command, path, URL, readiness marker, environment value, and probe required - * by their application. - */ -export class ApiTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/auth-test-server.ts b/packages/cellix/serenity-framework/src/servers/auth-test-server.ts deleted file mode 100644 index 57891aabd..000000000 --- a/packages/cellix/serenity-framework/src/servers/auth-test-server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; - -/** Options used by {@link AuthTestServer}. */ -export type AuthTestServerOptions = ProcessTestServerOptions; - -/** - * Process-backed authentication server for verification suites. - * - * Consumers provide the complete descriptor so the framework remains ignorant - * of local auth tools, hostnames, ports, and startup commands. - */ -export class AuthTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts b/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts deleted file mode 100644 index b219c6700..000000000 --- a/packages/cellix/serenity-framework/src/servers/azurite-test-server.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; - -/** Options used by {@link AzuriteTestServer}. */ -export type AzuriteTestServerOptions = ProcessTestServerOptions; - -/** - * Process-backed Azurite server for verification suites. - * - * All app-specific command, port, environment, and readiness details are - * supplied by the consumer so the framework package stays ignorant of local - * workspace conventions. - */ -export class AzuriteTestServer extends ProcessTestServer {} diff --git a/packages/cellix/serenity-framework/src/servers/index.ts b/packages/cellix/serenity-framework/src/servers/index.ts index 18a418b72..995ce4500 100644 --- a/packages/cellix/serenity-framework/src/servers/index.ts +++ b/packages/cellix/serenity-framework/src/servers/index.ts @@ -1,18 +1,8 @@ -export type { ApiTestServerOptions } from './api-test-server.ts'; -export { ApiTestServer } from './api-test-server.ts'; export type { ApolloGraphQLTestServerOptions } from './apollo-graphql-test-server.ts'; export { ApolloGraphQLTestServer } from './apollo-graphql-test-server.ts'; -export type { AuthTestServerOptions } from './auth-test-server.ts'; -export { AuthTestServer } from './auth-test-server.ts'; -export type { AzuriteTestServerOptions } from './azurite-test-server.ts'; -export { AzuriteTestServer } from './azurite-test-server.ts'; export type { MongoMemorySeedContext, MongoMemorySeedDataFunction, MongoMemoryTestServerOptions } from './mongo-memory-test-server.ts'; export { MongoMemoryTestServer } from './mongo-memory-test-server.ts'; export { createSpawnEnvironment } from './process-environment.ts'; export type { ProcessHealthProbe, ProcessTestServerOptions } from './process-test-server.ts'; export { ProcessTestServer } from './process-test-server.ts'; export type { SeedDataFunction, TestServer } from './test-server.ts'; -export type { TestServerGroupOptions } from './test-server-group.ts'; -export { TestServerGroup } from './test-server-group.ts'; -export type { UiPortalTestServerOptions } from './ui-portal-test-server.ts'; -export { UiPortalTestServer } from './ui-portal-test-server.ts'; diff --git a/packages/cellix/serenity-framework/src/servers/server-group.test.ts b/packages/cellix/serenity-framework/src/servers/server-group.test.ts deleted file mode 100644 index 81fb1659d..000000000 --- a/packages/cellix/serenity-framework/src/servers/server-group.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { type TestServer, TestServerGroup } from './index.ts'; - -class RecordingServer implements TestServer { - private readonly name: string; - private readonly calls: string[]; - - constructor(name: string, calls: string[]) { - this.name = name; - this.calls = calls; - } - - start(): Promise { - this.calls.push(`start:${this.name}`); - return Promise.resolve(); - } - - stop(): Promise { - this.calls.push(`stop:${this.name}`); - return Promise.resolve(); - } - - isRunning(): boolean { - return false; - } - - getUrl(): string { - return `https://${this.name}.example.test`; - } -} - -describe('TestServerGroup', () => { - it('starts required servers before variable UI portals and stops in reverse groups', async () => { - const calls: string[] = []; - const group = new TestServerGroup({ - required: [new RecordingServer('api', calls), new RecordingServer('auth', calls)], - uiPortals: [new RecordingServer('community', calls), new RecordingServer('staff', calls)], - }); - - await group.start(); - await group.stop(); - - expect(calls).toEqual(['start:api', 'start:auth', 'start:community', 'start:staff', 'stop:staff', 'stop:community', 'stop:auth', 'stop:api']); - }); -}); diff --git a/packages/cellix/serenity-framework/src/servers/test-server-group.ts b/packages/cellix/serenity-framework/src/servers/test-server-group.ts deleted file mode 100644 index 349e4f438..000000000 --- a/packages/cellix/serenity-framework/src/servers/test-server-group.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { TestServer } from './test-server.ts'; - -/** Options used by {@link TestServerGroup}. */ -export interface TestServerGroupOptions { - /** Servers required for the system under test. */ - required: TestServer[]; - - /** Variable UI portal servers. */ - uiPortals?: TestServer[]; -} - -/** - * Starts and stops required servers plus any number of UI portal servers. - * - * Required servers start before UI portals. Shutdown runs in reverse order. - */ -export class TestServerGroup implements TestServer { - private readonly required: TestServer[]; - private readonly uiPortals: TestServer[]; - - /** - * @param options Required servers and optional UI portals. - */ - constructor(options: TestServerGroupOptions) { - this.required = options.required; - this.uiPortals = options.uiPortals ?? []; - } - - /** Start required servers, then all UI portal servers. */ - async start(): Promise { - await Promise.all(this.required.map((server) => server.start())); - await Promise.all(this.uiPortals.map((server) => server.start())); - } - - /** Stop UI portals, then required servers. */ - async stop(): Promise { - await Promise.all([...this.uiPortals].reverse().map((server) => server.stop().catch(() => undefined))); - await Promise.all([...this.required].reverse().map((server) => server.stop().catch(() => undefined))); - } - - /** Return whether any grouped server reports as running. */ - isRunning(): boolean { - return [...this.required, ...this.uiPortals].some((server) => server.isRunning()); - } - - /** - * Server groups do not expose a single URL. - * - * @throws Error always. - */ - getUrl(): string { - throw new Error('TestServerGroup does not expose a single URL'); - } -} diff --git a/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts b/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts deleted file mode 100644 index 44221eaee..000000000 --- a/packages/cellix/serenity-framework/src/servers/ui-portal-test-server.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; - -/** Options used by {@link UiPortalTestServer}. */ -export type UiPortalTestServerOptions = ProcessTestServerOptions; - -/** - * Generic UI portal server for browser E2E suites. - * - * Consumers create one instance per portal and provide every command, path, - * readiness marker, environment value, and URL. The framework intentionally - * does not default to any dev server, executable, or portal naming convention. - */ -export class UiPortalTestServer extends ProcessTestServer {} diff --git a/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts index 0531dba81..117fe20f1 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts @@ -18,9 +18,7 @@ const infrastructure = ApiInfrastructure.using({ }, }); -interface InfrastructureState extends ApiInfrastructureState {} - -export function getState(): InfrastructureState { +export function getState(): ApiInfrastructureState { return infrastructure.getState(); } diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index 3bac51797..d0943ce3d 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -5,9 +5,9 @@ "private": true, "type": "module", "scripts": { - "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' cucumber-js", - "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' c8 cucumber-js", - "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/jsdom/register-asset-loader' c8 cucumber-js" + "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' cucumber-js", + "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' c8 cucumber-js", + "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm --import @cellix/serenity-framework/dom/register-asset-loader --import @cellix/serenity-framework/dom/setup' c8 cucumber-js" }, "dependencies": { "@apollo/client": "^3.13.9", diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts index 7b1b83168..670e14332 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/notes/header-notes.ts @@ -1,4 +1,6 @@ export interface HeaderUiNotes { + site: 'community' | 'staff'; + identityProviderUnreachable: boolean; signinRedirectCalled: boolean; consoleErrorCalled: boolean; fallbackInvoked: boolean; diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx index 7f1348c34..a69c19805 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/step-definitions/header-login.steps.tsx @@ -1,112 +1,80 @@ -import { mountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +import { Render } from '@cellix/serenity-framework/dom/render-in-dom'; import { Given, Then, When } from '@cucumber/cucumber'; -import { actorCalled, notes } from '@serenity-js/core'; +import { actorCalled, actorInTheSpotlight, notes } from '@serenity-js/core'; import React from 'react'; import { AuthContext, type AuthContextProps } from 'react-oidc-context'; import { SectionLayout as CommunitySectionLayout } from '../../../../../../ocom/ui-community-route-root/src/section-layout.tsx'; import { SectionLayout as StaffSectionLayout } from '../../../../../../ocom/ui-staff-route-root/src/section-layout.tsx'; import { wrapOcomComponent } from '../../../shared/ocom-component-wrapper.ts'; -import type { CellixUiWorld } from '../../../world.ts'; import type { HeaderUiNotes } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; type Site = 'community' | 'staff'; -interface HeaderScenarioState { - actorName: string; - site: Site; - identityProviderUnreachable: boolean; - originalConsoleError?: typeof console.error; - signinRedirectCalled: boolean; - errorCalled: boolean; +async function visitSite(actorName: string, site: Site): Promise { + await actorCalled(actorName).attemptsTo( + notes().set('site', site), + notes().set('identityProviderUnreachable', false), + notes().set('signinRedirectCalled', false), + notes().set('consoleErrorCalled', false), + notes().set('fallbackInvoked', false), + ); } -function getState(world: CellixUiWorld): HeaderScenarioState { - const state = (world as unknown as { __headerState?: HeaderScenarioState }).__headerState; - if (!state) { - throw new Error('Header scenario state has not been initialised — did the Given step run?'); - } - return state; -} - -function initState(world: CellixUiWorld, actorName: string, site: Site): HeaderScenarioState { - const state: HeaderScenarioState = { - actorName, - site, - identityProviderUnreachable: false, - signinRedirectCalled: false, - errorCalled: false, - }; - (world as unknown as { __headerState: HeaderScenarioState }).__headerState = state; - return state; -} - -Given('{word} visits the community site', async function (this: CellixUiWorld, actorName: string) { - const actor = actorCalled(actorName); - initState(this, actorName, 'community'); - await actor.attemptsTo(notes().set('signinRedirectCalled', false), notes().set('consoleErrorCalled', false), notes().set('fallbackInvoked', false)); +Given('{word} visits the community site', async (actorName: string) => { + await visitSite(actorName, 'community'); }); -Given('{word} visits the staff site', async function (this: CellixUiWorld, actorName: string) { - const actor = actorCalled(actorName); - initState(this, actorName, 'staff'); - await actor.attemptsTo(notes().set('signinRedirectCalled', false), notes().set('consoleErrorCalled', false), notes().set('fallbackInvoked', false)); +Given('{word} visits the staff site', async (actorName: string) => { + await visitSite(actorName, 'staff'); }); -Given('the identity provider is unreachable', function (this: CellixUiWorld) { - const state = getState(this); - state.identityProviderUnreachable = true; +Given('the identity provider is unreachable', async () => { + await actorInTheSpotlight().attemptsTo(notes().set('identityProviderUnreachable', true)); }); -When('{word} chooses to sign in', async function (this: CellixUiWorld, _actorName: string) { - const state = getState(this); +When('{word} chooses to sign in', async (actorName: string) => { + const actor = actorCalled(actorName); + const site = await actor.answer(notes().get('site')); + const identityProviderUnreachable = await actor.answer(notes().get('identityProviderUnreachable')); + let signinRedirectCalled = false; const signinRedirect = (): Promise => { - state.signinRedirectCalled = true; - if (state.identityProviderUnreachable) { - return Promise.reject(new Error('Simulated identity provider failure')); - } - return Promise.resolve(); + signinRedirectCalled = true; + return identityProviderUnreachable ? Promise.reject(new Error('Simulated identity provider failure')) : Promise.resolve(); }; const authValue = { signinRedirect } as unknown as AuthContextProps; - const PageComponent = state.site === 'community' ? CommunitySectionLayout : StaffSectionLayout; + const PageComponent = site === 'community' ? CommunitySectionLayout : StaffSectionLayout; const wrapped = React.createElement(AuthContext.Provider, { value: authValue }, React.createElement(PageComponent)); - state.originalConsoleError = console.error; - console.error = (..._args: unknown[]) => { - state.errorCalled = true; + const originalConsoleError = console.error; + let consoleErrorCalled = false; + console.error = () => { + consoleErrorCalled = true; }; - const rendered = mountComponent(wrapped, { wrapper: wrapOcomComponent() }); - this.setHeaderContainer(rendered.container); - try { - await ClickHeaderSignIn(rendered.container).performAs(actorCalled(state.actorName)); + await actor.attemptsTo(Render.component(wrapped, { wrapper: wrapOcomComponent() }), ClickHeaderSignIn()); } finally { - if (state.originalConsoleError) { - console.error = state.originalConsoleError; - } - const actor = actorCalled(state.actorName); + console.error = originalConsoleError; await actor.attemptsTo( - notes().set('signinRedirectCalled', state.signinRedirectCalled), - notes().set('consoleErrorCalled', state.errorCalled), - notes().set('fallbackInvoked', state.errorCalled), + notes().set('signinRedirectCalled', signinRedirectCalled), + notes().set('consoleErrorCalled', consoleErrorCalled), + notes().set('fallbackInvoked', consoleErrorCalled), ); } }); -Then('{word} is taken to the sign-in flow', async function (this: CellixUiWorld, actorName: string) { - const actor = actorCalled(actorName); - const called = await actor.answer(notes().get('signinRedirectCalled')); +Then('{word} is taken to the sign-in flow', async (actorName: string) => { + const called = await actorCalled(actorName).answer(notes().get('signinRedirectCalled')); if (!called) { throw new Error(`Expected ${actorName} to be taken to the sign-in flow, but the sign-in handler was not invoked`); } }); -Then('{word} can still reach the sign-in page', async function (this: CellixUiWorld, actorName: string) { - const actor = actorCalled(actorName); - const fallback = await actor.answer(notes().get('fallbackInvoked')); +Then('{word} can still reach the sign-in page', async (actorName: string) => { + const fallback = await actorCalled(actorName).answer(notes().get('fallbackInvoked')); if (!fallback) { throw new Error(`Expected ${actorName} to reach the sign-in page via the fallback path, but the fallback was not triggered`); } diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts index dbf767f8a..66c4f4745 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/authentication/tasks/click-header-sign-in.ts @@ -1,23 +1,23 @@ -import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; +import { DomPageAdapter } from '@cellix/serenity-framework/pages/dom'; import { TaskStep } from '@cellix/serenity-framework/serenity'; import { HomePage } from '@ocom-verification/verification-shared/pages'; -import { type Activity, Task } from '@serenity-js/core'; +import { type Actor, Task } from '@serenity-js/core'; import type { AcceptanceUiHomePage } from '../../../shared/page-contracts.ts'; -async function flushAsync(): Promise { - await new Promise((resolve) => { - setTimeout(resolve, 0); - }); +/** Let the sign-in handler's async work settle before assertions run. */ +async function flushPendingReactWork(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); } -export const ClickHeaderSignIn = (container: HTMLElement) => +export const ClickHeaderSignIn = (): Task => Task.where( '#actor clicks the sign-in button on the home page', - new TaskStep('#actor clicks the sign-in button', async () => { - const adapter = new JsdomPageAdapter(container); - const page: AcceptanceUiHomePage = new HomePage(adapter); + new TaskStep('#actor clicks the sign-in button', async (actor) => { + const page: AcceptanceUiHomePage = new HomePage(new DomPageAdapter(RenderInDom.as(actor).container)); await page.clickSignIn(); - await flushAsync(); - }) as Activity, + + await flushPendingReactWork(); + }), ); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts index c8f9fdf09..d3f7091cd 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/notes/community-notes.ts @@ -1,5 +1,4 @@ export interface CommunityUiNotes { communityName: string; formSubmitted: boolean; - lastValidationError: string; } diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts deleted file mode 100644 index 61865a446..000000000 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/questions/community-error-message.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { notes, Question } from '@serenity-js/core'; -import type { CommunityUiNotes } from '../notes/community-notes.ts'; - -export const CommunityErrorMessage = () => Question.about('the community form error message', (actor) => actor.answer(notes().get('lastValidationError'))); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx index ee426c518..0d3394237 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/step-definitions/create-community.steps.tsx @@ -1,125 +1,67 @@ -import { ActorName } from '@cellix/serenity-framework/cucumber/actor-name'; import { GherkinDataTable } from '@cellix/serenity-framework/cucumber/gherkin-data-table'; -import { mountComponent } from '@cellix/serenity-framework/jsdom/react-render'; -import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { Render, RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; +import { DomPageAdapter } from '@cellix/serenity-framework/pages/dom'; import { type DataTable, Given, Then, When } from '@cucumber/cucumber'; import { CommunityPage } from '@ocom-verification/verification-shared/pages'; -import { actorCalled, notes } from '@serenity-js/core'; +import { actorCalled, actorInTheSpotlight, notes } from '@serenity-js/core'; import { CommunityCreate } from '../../../../../../ocom/ui-community-route-accounts/src/components/community-create.tsx'; import { wrapOcomComponent } from '../../../shared/ocom-component-wrapper.ts'; import type { AcceptanceUiCommunityPage } from '../../../shared/page-contracts.ts'; -import type { CellixUiWorld } from '../../../world.ts'; import type { CommunityUiNotes } from '../notes/community-notes.ts'; import { CommunityCreatedFlag } from '../questions/community-created-flag.ts'; -import { CommunityErrorMessage } from '../questions/community-error-message.ts'; import { CommunityName } from '../questions/community-name.ts'; import { CreateCommunity } from '../tasks/create-community.ts'; -Given('{word} is an authenticated community owner', async function (this: CellixUiWorld, actorName: string) { - this.setCommunityActorName(actorName); +Given('{word} is an authenticated community owner', async (actorName: string) => { const actor = actorCalled(actorName); const onSave = async (values: { name: string }): Promise => { - await actor.attemptsTo(notes().set('formSubmitted', true), notes().set('communityName', values.name ?? ''), notes().set('lastValidationError', '')); + await actor.attemptsTo(notes().set('formSubmitted', true), notes().set('communityName', values.name ?? '')); }; - const rendered = mountComponent(, { wrapper: wrapOcomComponent() }); - this.setCommunityContainer(rendered.container); - - await actor.attemptsTo(notes().set('formSubmitted', false), notes().set('communityName', ''), notes().set('lastValidationError', '')); + await actor.attemptsTo(notes().set('formSubmitted', false), notes().set('communityName', ''), Render.component(, { wrapper: wrapOcomComponent() })); }); -When('{word} creates a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { - this.setCommunityActorName(actorName); - const actor = actorCalled(actorName); +When('{word} creates a community with:', async (actorName: string, dataTable: DataTable) => { const { name: communityName = '' } = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); - - await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); + await actorCalled(actorName).attemptsTo(CreateCommunity(communityName)); }); -When('{word} attempts to create a community with:', async function (this: CellixUiWorld, actorName: string, dataTable: DataTable) { - this.setCommunityActorName(actorName); - const actor = actorCalled(actorName); +When('{word} attempts to create a community with:', async (actorName: string, dataTable: DataTable) => { const { name: communityName = '' } = GherkinDataTable.from(dataTable).rowsHash<{ name?: string }>(); - - await actor.attemptsTo(CreateCommunity(this.getCommunityContainer(), communityName)); + await actorCalled(actorName).attemptsTo(CreateCommunity(communityName)); }); -Then('the community should be created successfully', async function (this: CellixUiWorld) { - const actor = actorCalled(this.getCommunityActorName()); - const submitted = await actor.answer(CommunityCreatedFlag()); - +Then('the community should be created successfully', async () => { + const submitted = await actorInTheSpotlight().answer(CommunityCreatedFlag()); if (!submitted) { throw new Error('Expected community form to be submitted'); } }); -Then('the community name should be {string}', async function (this: CellixUiWorld, expectedName: string) { - const actor = actorCalled(this.getCommunityActorName()); - const name = await actor.answer(CommunityName()); - +Then('the community name should be {string}', async (expectedName: string) => { + const name = await actorInTheSpotlight().answer(CommunityName()); if (name !== expectedName) { throw new Error(`Expected community name "${expectedName}" but got "${name}"`); } }); -Then('{word} should see a community error for {string}', async function (this: CellixUiWorld, actorName: string, fieldName: string) { - const resolvedName = ActorName.resolve(actorName, { defaultName: this.getCommunityActorName() }); +Then('{word} should see a community error for {string}', async (_actorName: string, fieldName: string) => { + const actor = actorInTheSpotlight(); + const page: AcceptanceUiCommunityPage = new CommunityPage(new DomPageAdapter(RenderInDom.as(actor).container)); + const errorText = (await page.firstValidationError.textContent()) ?? ''; - const container = this.getCommunityContainer(); - const adapter = new JsdomPageAdapter(container); - const page = new CommunityPage(adapter) as AcceptanceUiCommunityPage; + const isFieldMentioned = errorText.toLowerCase().includes(fieldName.toLowerCase()); + const isValidationPattern = /cannot be empty|required|missing|invalid|must not be empty|please input/i.test(errorText); - let storedError: string | undefined; - try { - const errorEl = await page.firstValidationError; - if (errorEl) { - storedError = (await errorEl.textContent()) ?? undefined; - } - } catch { - const actor = actorCalled(resolvedName); - try { - storedError = await actor.answer(CommunityErrorMessage()); - } catch { - // No error found - } + if (!errorText || (!isFieldMentioned && !isValidationPattern)) { + throw new Error(`Expected a validation error related to "${fieldName}", but got: "${errorText}"`); } - - if (storedError) { - const lowerError = storedError.toLowerCase(); - const lowerField = fieldName.toLowerCase(); - const isFieldMentioned = lowerError.includes(lowerField); - const isValidationPattern = /cannot be empty|required|missing|invalid|must not be empty|please input/i.test(storedError); - - if (!isFieldMentioned && !isValidationPattern) { - throw new Error(`Expected a validation error related to "${fieldName}", but got: "${storedError}"`); - } - return; - } - - const errorElements = container.querySelectorAll('.ant-form-item-explain-error'); - if (errorElements.length > 0) { - return; - } - - throw new Error(`Expected a validation error for "${fieldName}" but none was found`); }); -Then('no community should be created', async function (this: CellixUiWorld) { - let hasValidationError = false; - try { - const actor = actorCalled(this.getCommunityActorName()); - const storedError = await actor.answer(CommunityErrorMessage()); - hasValidationError = !!storedError; - } catch { - // No error stored — check DOM - } - - if (!hasValidationError) { - const container = this.getCommunityContainer(); - const errorElements = container.querySelectorAll('.ant-form-item-explain-error'); - if (errorElements.length === 0) { - throw new Error('Expected a validation error to prevent community creation, but no error was found.'); - } +Then('no community should be created', async () => { + const submitted = await actorInTheSpotlight().answer(CommunityCreatedFlag()); + if (submitted) { + throw new Error('Expected no community to be created, but the form was submitted'); } }); diff --git a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts index 305801dc3..39168253b 100644 --- a/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-ui/src/contexts/community/tasks/create-community.ts @@ -1,28 +1,25 @@ -import { JsdomPageAdapter } from '@cellix/serenity-framework/pages/jsdom'; +import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; +import { DomPageAdapter } from '@cellix/serenity-framework/pages/dom'; import { TaskStep } from '@cellix/serenity-framework/serenity'; import { CommunityPage } from '@ocom-verification/verification-shared/pages'; -import { type Activity, Task } from '@serenity-js/core'; +import { type Actor, Task } from '@serenity-js/core'; import type { AcceptanceUiCommunityPage } from '../../../shared/page-contracts.ts'; -async function flushAsync(): Promise { - await new Promise((resolve) => { - setTimeout(resolve, 0); - }); - await new Promise((resolve) => { - setTimeout(resolve, 0); - }); +/** Let the form's async `onFinish`/`onSave` handlers settle before assertions run. */ +async function flushPendingReactWork(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); } -export const CreateCommunity = (container: HTMLElement, name: string) => +export const CreateCommunity = (name: string): Task => Task.where( - `#actor fills community name "${name}" and submits`, - new TaskStep(`#actor submits community name "${name}"`, async () => { - const adapter = new JsdomPageAdapter(container); - const page: AcceptanceUiCommunityPage = new CommunityPage(adapter); + `#actor creates a community named "${name}"`, + new TaskStep(`#actor fills the community name "${name}" and submits`, async (actor) => { + const page: AcceptanceUiCommunityPage = new CommunityPage(new DomPageAdapter(RenderInDom.as(actor).container)); await page.fillName(name); await page.clickCreate(); - await flushAsync(); - }) as Activity, + await flushPendingReactWork(); + }), ); diff --git a/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts index 0c531114f..d45035281 100644 --- a/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts @@ -1,7 +1,8 @@ import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; -import { unmountComponent } from '@cellix/serenity-framework/jsdom/react-render'; +import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; import { getTimeout } from '@cellix/serenity-framework/settings'; import { After } from '@cucumber/cucumber'; +import { actorInTheSpotlight } from '@serenity-js/core'; import type { CellixUiWorld } from '../world.ts'; registerWorldLifecycleHooks({ @@ -13,5 +14,9 @@ registerWorldLifecycleHooks({ }); After({ timeout: getTimeout('uiCleanup') }, () => { - unmountComponent(); + try { + RenderInDom.as(actorInTheSpotlight()).unmount(); + } catch { + /* No component was rendered in this scenario. */ + } }); diff --git a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts index cc39388de..1b7c8d7b3 100644 --- a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts @@ -3,7 +3,6 @@ * Cucumber imports this file, which then loads all context-specific step definitions. */ -import '@cellix/serenity-framework/jsdom/setup'; import '../shared/cucumber-lifecycle-hooks.ts'; import '../contexts/community/step-definitions/index.ts'; import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/acceptance-ui/src/world.ts b/packages/ocom-verification/acceptance-ui/src/world.ts index 986ea574d..747d7249f 100644 --- a/packages/ocom-verification/acceptance-ui/src/world.ts +++ b/packages/ocom-verification/acceptance-ui/src/world.ts @@ -1,53 +1,17 @@ -import { ManagedSerenityWorld, type ManagedSerenityWorldOptions } from '@cellix/serenity-framework/cucumber'; +import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; -import { type IWorldOptions, setWorldConstructor } from '@cucumber/cucumber'; -const uiWorldConfig: ManagedSerenityWorldOptions> = { +export const CellixUiWorld = registerManagedSerenityWorld>({ infrastructure: { ensureStarted: () => Promise.resolve(), getState: () => ({}), }, - createCast: () => new SerenityCast({ useNotepad: true }), -}; - -export class CellixUiWorld extends ManagedSerenityWorld> { - private communityContainer: HTMLElement | null = null; - private communityActorName = ''; - private headerContainer: HTMLElement | null = null; - - constructor(options: IWorldOptions) { - super(options, uiWorldConfig); - } - - setCommunityContainer(container: HTMLElement): void { - this.communityContainer = container; - } - - getCommunityContainer(): HTMLElement { - if (!this.communityContainer) { - throw new Error('No community container available — did the Given step run?'); - } - return this.communityContainer; - } - - setCommunityActorName(actorName: string): void { - this.communityActorName = actorName; - } - - getCommunityActorName(): string { - return this.communityActorName; - } - - setHeaderContainer(container: HTMLElement): void { - this.headerContainer = container; - } - - getHeaderContainer(): HTMLElement { - if (!this.headerContainer) { - throw new Error('No header container available — did the Given step run?'); - } - return this.headerContainer; - } -} - -setWorldConstructor(CellixUiWorld); + createCast: () => + new SerenityCast({ + useNotepad: true, + abilities: [() => new RenderInDom()], + }), +}); + +export type CellixUiWorld = InstanceType; diff --git a/packages/ocom-verification/acceptance-ui/tsconfig.json b/packages/ocom-verification/acceptance-ui/tsconfig.json index a1ea4a843..5966e2dba 100644 --- a/packages/ocom-verification/acceptance-ui/tsconfig.json +++ b/packages/ocom-verification/acceptance-ui/tsconfig.json @@ -13,5 +13,5 @@ "rootDir": "../..", "outDir": "./dist" }, - "include": ["src/**/*.ts", "src/**/*.tsx", "../../cellix/serenity-framework/src/jsdom/css-module-types.d.ts", "../../ocom/ui-community-route-root/src/**/*.tsx", "../../ocom/ui-staff-route-root/src/**/*.tsx"] + "include": ["src/**/*.ts", "src/**/*.tsx", "../../cellix/serenity-framework/src/dom/css-module-types.d.ts", "../../ocom/ui-community-route-root/src/**/*.tsx", "../../ocom/ui-staff-route-root/src/**/*.tsx"] } diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index d03d4f5b0..7f3035b8b 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -65,7 +65,7 @@ When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorNam const { browser } = infra.getState(); if (!browser) throw new Error('Browser not launched'); - const baseUrl = s.site === 'community' ? (infra.getState().communityBaseUrl ?? 'https://ownercommunity.localhost:1355') : (infra.getState().staffBaseUrl ?? 'https://staff.ownercommunity.localhost:1355'); + const baseUrl = s.site === 'community' ? (infra.getUiBaseUrl('community') ?? 'https://ownercommunity.localhost:1355') : (infra.getUiBaseUrl('staff') ?? 'https://staff.ownercommunity.localhost:1355'); // Fresh unauthenticated context — isolated from the pre-auth context // used by other test suites. Cleaned up in the Then step after verification. diff --git a/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts index 318008c19..2fbdc6d91 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts @@ -32,18 +32,12 @@ const infrastructure = E2EInfrastructure.using({ .addUiPortal('community', createCommunityUiPortalServer()) .addUiPortal('staff', createStaffUiPortalServer()); -interface InfrastructureState extends E2EInfrastructureState { - staffBaseUrl: string | undefined; - communityBaseUrl: string | undefined; +export function getState(): E2EInfrastructureState { + return infrastructure.getState(); } -export function getState(): InfrastructureState { - const state = infrastructure.getState(); - return { - ...state, - communityBaseUrl: state.uiPortalBaseUrls['community'], - staffBaseUrl: state.uiPortalBaseUrls['staff'], - }; +export function getUiBaseUrl(site: 'community' | 'staff'): string | undefined { + return infrastructure.getState().uiPortalBaseUrls[site]; } export async function resetScenarioState(): Promise { diff --git a/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts b/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts index 3c51cb5c0..d8acc69ab 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts @@ -1,5 +1,5 @@ import { join } from 'node:path'; -import { ApiTestServer, AuthTestServer, AzuriteTestServer, UiPortalTestServer } from '@cellix/serenity-framework/servers'; +import { ProcessTestServer } from '@cellix/serenity-framework/servers'; import { appPaths } from './environment/app-paths.ts'; import { e2eEnv, getPortlessDevScript } from './environment/dev-script.ts'; import { buildUrl, cleanupTestEnvironment, getHostnames, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer } from './environment/test-environment.ts'; @@ -9,8 +9,8 @@ const hostnames = getHostnames(); export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer }; -export function createTestApiServer(getMongoConnectionString: () => string): ApiTestServer { - return new ApiTestServer({ +export function createTestApiServer(getMongoConnectionString: () => string): ProcessTestServer { + return new ProcessTestServer({ cwd: appPaths.apiDir, executable: 'pnpm', extraEnv: () => @@ -44,8 +44,8 @@ export function createTestApiServer(getMongoConnectionString: () => string): Api }); } -export function createTestAzuriteServer(): AzuriteTestServer { - return new AzuriteTestServer({ +export function createTestAzuriteServer(): ProcessTestServer { + return new ProcessTestServer({ cwd: appPaths.apiDir, executable: 'node', extraEnv: () => { @@ -63,8 +63,8 @@ export function createTestAzuriteServer(): AzuriteTestServer { }); } -export function createTestOAuth2Server(): AuthTestServer { - return new AuthTestServer({ +export function createTestOAuth2Server(): ProcessTestServer { + return new ProcessTestServer({ cwd: appPaths.oauth2MockDir, executable: 'pnpm', getUrl: () => mockOidcIssuer, @@ -77,8 +77,8 @@ export function createTestOAuth2Server(): AuthTestServer { }); } -export function createCommunityUiPortalServer(): UiPortalTestServer { - return new UiPortalTestServer({ +export function createCommunityUiPortalServer(): ProcessTestServer { + return new ProcessTestServer({ cwd: appPaths.uiCommunityDir, executable: 'pnpm', extraEnv: () => ({ @@ -92,8 +92,8 @@ export function createCommunityUiPortalServer(): UiPortalTestServer { }); } -export function createStaffUiPortalServer(): UiPortalTestServer { - return new UiPortalTestServer({ +export function createStaffUiPortalServer(): ProcessTestServer { + return new ProcessTestServer({ cwd: appPaths.uiStaffDir, executable: 'pnpm', extraEnv: () => ({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faafb27b6..0d6ffe217 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,7 +237,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/api: dependencies: @@ -316,7 +316,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/docs: dependencies: @@ -386,7 +386,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/server-mongodb-memory-mock: dependencies: @@ -439,7 +439,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-community: dependencies: @@ -560,7 +560,7 @@ importers: version: 0.28.0(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-staff: dependencies: @@ -660,7 +660,7 @@ importers: version: 0.28.0(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/api-services-spec: devDependencies: @@ -696,7 +696,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-rolldown: devDependencies: @@ -720,7 +720,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-typescript: {} @@ -740,7 +740,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/domain-seedwork: devDependencies: @@ -761,7 +761,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/event-bus-seedwork-node: dependencies: @@ -829,7 +829,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/graphql-core: dependencies: @@ -857,7 +857,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/mongoose-seedwork: dependencies: @@ -894,7 +894,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/serenity-framework: dependencies: @@ -910,6 +910,9 @@ importers: '@cucumber/messages': specifier: 'catalog:' version: 32.3.1 + '@happy-dom/global-registrator': + specifier: ^20.9.0 + version: 20.9.0 '@serenity-js/core': specifier: 'catalog:' version: 3.42.2 @@ -919,9 +922,9 @@ importers: graphql-depth-limit: specifier: ^1.1.0 version: 1.1.0(graphql@16.12.0) - jsdom: - specifier: 'catalog:' - version: 26.1.0 + happy-dom: + specifier: ^20.9.0 + version: 20.9.0 mongodb: specifier: 'catalog:' version: 6.18.0 @@ -961,7 +964,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/server-mongodb-memory-mock-seedwork: dependencies: @@ -1014,7 +1017,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/ui-core: dependencies: @@ -1093,7 +1096,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1269,7 +1272,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/e2e-tests: dependencies: @@ -1377,7 +1380,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/context-spec: dependencies: @@ -1430,7 +1433,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/domain: dependencies: @@ -1491,7 +1494,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/event-handler: dependencies: @@ -1553,7 +1556,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/graphql-handler: dependencies: @@ -1596,7 +1599,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/persistence: dependencies: @@ -1645,7 +1648,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/rest: dependencies: @@ -1707,7 +1710,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-blob-storage: dependencies: @@ -1815,7 +1818,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-token-validation: dependencies: @@ -1843,7 +1846,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-accounts: dependencies: @@ -1937,7 +1940,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-admin: dependencies: @@ -2031,7 +2034,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-root: dependencies: @@ -2101,7 +2104,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-shared: dependencies: @@ -2180,7 +2183,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-shared: dependencies: @@ -2277,7 +2280,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-community-management: dependencies: @@ -2320,7 +2323,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-finance: dependencies: @@ -2363,7 +2366,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-root: dependencies: @@ -2406,7 +2409,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-tech-admin: dependencies: @@ -2449,7 +2452,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-user-management: dependencies: @@ -2492,7 +2495,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-shared: dependencies: @@ -2541,7 +2544,7 @@ importers: version: 8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -4859,6 +4862,10 @@ packages: '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@happy-dom/global-registrator@20.9.0': + resolution: {integrity: sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw==} + engines: {node: '>=20.0.0'} + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -6744,6 +6751,9 @@ packages: '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} @@ -8409,6 +8419,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} hasBin: true @@ -9020,6 +9034,10 @@ packages: handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + happy-dom@20.9.0: + resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} + engines: {node: '>=20.0.0'} + has-ansi@4.0.1: resolution: {integrity: sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==} engines: {node: '>=8'} @@ -13322,6 +13340,10 @@ packages: engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -13664,7 +13686,7 @@ snapshots: minimist: 1.2.8 parsecurrency: 1.1.1 ts-morph: 27.0.2 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@ant-design/cli@6.3.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)': dependencies: @@ -17012,6 +17034,14 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@happy-dom/global-registrator@20.9.0': + dependencies: + '@types/node': 24.10.1 + happy-dom: 20.9.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@inquirer/external-editor@1.0.3(@types/node@22.19.15)': dependencies: chardet: 2.1.1 @@ -18525,7 +18555,7 @@ snapshots: '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/runner': 4.1.6 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom @@ -18541,7 +18571,7 @@ snapshots: '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/runner': 4.1.6 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom @@ -19012,6 +19042,8 @@ snapshots: '@types/webidl-conversions@7.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/whatwg-url@11.0.5': dependencies: '@types/webidl-conversions': 7.0.3 @@ -19086,7 +19118,7 @@ snapshots: '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -19100,7 +19132,7 @@ snapshots: '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -19116,7 +19148,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.1 transitivePeerDependencies: - bufferutil @@ -19134,7 +19166,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.20.1 transitivePeerDependencies: - bufferutil @@ -19154,7 +19186,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -20947,6 +20979,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + errno@0.1.8: dependencies: prr: 1.0.1 @@ -21762,6 +21796,18 @@ snapshots: handle-thing@2.0.1: {} + happy-dom@20.9.0: + dependencies: + '@types/node': 24.10.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-ansi@4.0.1: dependencies: ansi-regex: 4.1.1 @@ -26559,7 +26605,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -26586,11 +26632,12 @@ snapshots: '@types/node': 22.19.15 '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + happy-dom: 20.9.0 jsdom: 26.1.0 transitivePeerDependencies: - msw - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.6 '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -26617,6 +26664,7 @@ snapshots: '@types/node': 24.10.1 '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + happy-dom: 20.9.0 jsdom: 26.1.0 transitivePeerDependencies: - msw @@ -26793,6 +26841,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} whatwg-url@14.2.0: From c0ab79431ea2f914283b670a22f653762cc6e149 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 3 Jun 2026 11:19:31 -0400 Subject: [PATCH 16/40] made changes regarding infrastructure patterns, simplified what was needed, and allowed more flexability for passing in of servers, gives user control of what they need. ensured api doesnt break across worktrees --- knip.json | 2 +- packages/cellix/serenity-framework/README.md | 43 +- .../cellix/serenity-framework/manifest.md | 4 +- .../src/infrastructure/api-infrastructure.ts | 87 ++-- .../src/infrastructure/e2e-infrastructure.ts | 415 +++++++++++------- .../src/infrastructure/index.ts | 13 +- .../src/infrastructure/infrastructure.test.ts | 66 ++- .../src/cucumber-lifecycle-hooks.ts | 28 ++ ...ed-infrastructure.ts => infrastructure.ts} | 5 +- .../src/shared/cucumber-lifecycle-hooks.ts | 24 - .../src/{shared => }/test-server-factories.ts | 2 +- .../acceptance-api/src/world.ts | 16 +- .../src/cucumber-lifecycle-hooks.ts | 25 ++ .../src/shared/cucumber-lifecycle-hooks.ts | 22 - .../src/step-definitions/index.ts | 1 - .../acceptance-ui/src/world.ts | 3 + .../step-definitions/header-login.steps.ts | 17 +- .../e2e-tests/src/cucumber-lifecycle-hooks.ts | 28 ++ .../e2e-tests/src/infrastructure.ts | 44 ++ .../src/shared/cucumber-lifecycle-hooks.ts | 24 - .../src/shared/environment/worktree-ports.ts | 26 -- .../src/shared/shared-infrastructure.ts | 54 --- .../e2e-tests/src/step-definitions/index.ts | 1 - .../src/{shared => }/test-server-factories.ts | 12 +- .../ocom-verification/e2e-tests/src/world.ts | 10 +- .../verification-shared/package.json | 1 + .../src/environment/index.ts | 1 + .../src/environment/worktree-ports.ts | 39 ++ pnpm-lock.yaml | 42 +- pnpm-workspace.yaml | 2 +- 30 files changed, 589 insertions(+), 468 deletions(-) create mode 100644 packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts rename packages/ocom-verification/acceptance-api/src/{shared/shared-infrastructure.ts => infrastructure.ts} (85%) delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts rename packages/ocom-verification/acceptance-api/src/{shared => }/test-server-factories.ts (94%) create mode 100644 packages/ocom-verification/acceptance-ui/src/cucumber-lifecycle-hooks.ts delete mode 100644 packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts create mode 100644 packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts create mode 100644 packages/ocom-verification/e2e-tests/src/infrastructure.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts rename packages/ocom-verification/e2e-tests/src/{shared => }/test-server-factories.ts (84%) create mode 100644 packages/ocom-verification/verification-shared/src/environment/index.ts create mode 100644 packages/ocom-verification/verification-shared/src/environment/worktree-ports.ts diff --git a/knip.json b/knip.json index 859113a5c..e3de3d8c4 100644 --- a/knip.json +++ b/knip.json @@ -83,7 +83,7 @@ "ignoreUnresolved": ["progress-bar"] }, "packages/ocom-verification/e2e-tests": { - "entry": ["cucumber.js", "src/world.ts", "src/contexts/**/step-definitions/**/*.steps.ts", "src/shared/environment/**/*.ts", "src/shared/abilities/**/*.ts", "src/shared/test-server-factories.ts"], + "entry": ["cucumber.js", "src/world.ts", "src/contexts/**/step-definitions/**/*.steps.ts", "src/shared/environment/**/*.ts", "src/shared/abilities/**/*.ts"], "project": ["src/**/*.ts"], "ignoreUnresolved": ["progress-bar"] }, diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index abacc7c2f..b8ddaa3da 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -32,45 +32,48 @@ import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwrig ## Server composition -Use generic server descriptors for app-specific processes, then load them into the suite infrastructure manager instead of hand-writing suite startup code. +Compose only the servers a suite needs by chaining `addServer` and `addUiPortal`. The framework starts them in dependency order (servers whose `dependsOn` is satisfied start in parallel), resets per-scenario state, sets up the browser when a UI portal exists, and tears everything down. Each application registers its own set — fewer or more servers — without changing the framework. ```ts import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; -import { ProcessTestServer } from '@cellix/serenity-framework/servers'; - -const communityPortal = new ProcessTestServer({ - portalName: 'community', - cwd: '/repo/apps/ui-community', - getUrl: () => 'https://community.localhost:1355', - spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], -}); +import { MongoMemoryTestServer, ProcessTestServer } from '@cellix/serenity-framework/servers'; export const infrastructure = E2EInfrastructure - .using({ - mongoServer: { dbName, port, replSetName, seedData }, - azuriteServer, - authServer, - createApiServer: ({ getMongoConnectionString }) => + .create({ + // Shared across every portal context; baseURL is supplied per portal. + browserContextOptions: { ignoreHTTPSErrors: true }, + }) + .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { + resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), + }) + .addServer('auth', () => createAuthServer()) + .addServer( + 'api', + (ctx) => new ProcessTestServer({ serverName: 'Api', executable: 'pnpm', spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], cwd: '/repo/apps/api', - extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: getMongoConnectionString() }), + extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: ctx.server('mongo').getConnectionString() }), getUrl: () => 'https://api.localhost:1355/api/graphql', readyMarker: 'Functions:', }), - launchBrowser: () => playwright.chromium.launch({ headless: true }), - }) - .addUiPortal('community', communityPortal) - .addUiPortal('staff', staffPortal); + { dependsOn: ['mongo'] }, + ) + .addUiPortal('community', () => createCommunityPortal()) + .addUiPortal('staff', () => createStaffPortal()); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); await infrastructure.stopAll(); + +// Each portal carries its own context recipe — baseURL is that portal's URL — +// so a scenario opens a context for whichever portal it needs, symmetrically: +const staffContext = await infrastructure.newPortalContext('staff'); ``` -The framework never imports app paths or app-specific environment helpers. Pass those values into descriptors from the consumer package. +A server factory receives a context so a dependent server (such as the API) can read a started dependency's runtime state (such as the database connection string). Each UI portal owns its browser-context recipe, so `newPortalContext(name)` scopes navigation to that portal without any portal being special-cased. The framework never imports app paths or app-specific environment helpers — pass those into the factories from the consumer package. ## API acceptance infrastructure diff --git a/packages/cellix/serenity-framework/manifest.md b/packages/cellix/serenity-framework/manifest.md index 0e2f58705..9dae46cf9 100644 --- a/packages/cellix/serenity-framework/manifest.md +++ b/packages/cellix/serenity-framework/manifest.md @@ -15,7 +15,7 @@ This package owns generic verification infrastructure only: - Adapter-backed page-object base contracts - Timeout utilities - Configurable process, Apollo GraphQL, and Mongo memory server lifecycle utilities -- API acceptance and browser E2E infrastructure managers that create MongoDB from options and compose consumer-owned server factories +- API acceptance and browser E2E infrastructure managers that compose consumer-owned server factories; the E2E manager starts a freely-composed, dependency-ordered server set and the browser ## Non-goals @@ -39,7 +39,7 @@ This package owns generic verification infrastructure only: - `@cellix/serenity-framework/serenity`: `TaskStep`, `SerenityCast` - `@cellix/serenity-framework/serenity/browser`: `BrowseTheWeb` - `@cellix/serenity-framework/infrastructure/api`: API acceptance infrastructure manager with MongoDB options, optional Mongoose service management, and an API server factory -- `@cellix/serenity-framework/infrastructure/e2e`: browser E2E infrastructure manager with MongoDB options, required Azurite/auth servers, an API server factory, and chainable UI portals +- `@cellix/serenity-framework/infrastructure/e2e`: browser E2E infrastructure manager that composes a dependency-ordered server set via chainable `addServer`/`addUiPortal` and sets up the browser - `@cellix/serenity-framework/servers`: generic server lifecycle classes and interfaces - `@cellix/serenity-framework/settings`: timeout helpers diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts index fe0fb64a8..8744c626c 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -34,74 +34,83 @@ export interface ApiInfrastructureMongooseOptions { +/** Context supplied to the GraphQL server factory. */ +export interface GraphqlServerFactoryContext { /** Framework-owned MongoDB server, or `undefined` when none is configured. */ mongoServer: MongoMemoryTestServer | undefined; /** Resolve the MongoDB connection string. Throws if no `mongoServer` is configured. */ getMongoConnectionString: () => string; - /** Resolve the managed Mongoose service, when configured. */ + /** Resolve the managed Mongoose service. Throws if no `mongoose` option is configured. */ getMongooseService: () => TMongooseService; } -/** Factory that creates an API server after framework-owned resources are available. */ -export type ApiServerFactory = (context: ApiServerFactoryContext) => TestServer; +/** Factory that creates the GraphQL server after framework-owned resources are available. */ +export type GraphqlServerFactory = (context: GraphqlServerFactoryContext) => TestServer; /** Factory used to create the framework-owned MongoDB server. */ export type ApiMongoServerFactory = (options: MongoMemoryTestServerOptions) => MongoMemoryTestServer; /** State exposed by {@link ApiInfrastructure}. */ export interface ApiInfrastructureState { - /** Running MongoDB server for the suite. */ + /** Running MongoDB server, or `undefined` when none is configured. */ mongoServer: MongoMemoryTestServer | undefined; - /** Running API server for the suite. */ - apiServer: TestServer | undefined; + /** Running GraphQL server for the suite. */ + graphqlServer: TestServer | undefined; - /** Running Mongoose service, when configured. */ + /** Running Mongoose service, or `undefined` when none is configured. */ mongooseService: TMongooseService | undefined; - /** API endpoint URL, when the API server has started. */ - apiUrl: string | undefined; + /** GraphQL endpoint URL, when the server has started. */ + graphqlUrl: string | undefined; } /** Options used by {@link ApiInfrastructure.using}. */ export interface ApiInfrastructureOptions { - /** MongoDB memory server options. Omit when the suite does not need MongoDB. */ + /** MongoDB memory server options. Omit when the suite does not need a database. */ mongoServer?: MongoMemoryTestServerOptions; /** Optional MongoDB server factory. Defaults to `new MongoMemoryTestServer(options)`. */ createMongoServer?: ApiMongoServerFactory; - /** Optional Mongoose service managed between MongoDB and the API server. */ + /** Optional Mongoose service managed between MongoDB and the GraphQL server. Requires `mongoServer`. */ mongoose?: ApiInfrastructureMongooseOptions; - /** Factory that creates the API server with access to framework-owned resources. */ - createApiServer: ApiServerFactory; + /** Factory that creates the GraphQL server with access to framework-owned resources. */ + createGraphqlServer: GraphqlServerFactory; } /** * Lifecycle manager for API acceptance tests. * - * Use this when a suite needs only the always-present API acceptance base: - * MongoDB plus an API server. Consumers configure the concrete server objects - * with app-specific schema, context, services, seed data, and environment - * values before passing factories to the framework. + * The only always-present piece is a consumer-provided GraphQL server. MongoDB + * and a Mongoose service are both optional — omit them for an app with no + * database, or supply `mongoServer` (and optionally `mongoose`) when the GraphQL + * server needs persistence. Consumers configure the concrete server objects with + * app-specific schema, context, services, seed data, and environment values + * before passing factories to the framework. * * @example * ```ts - * export const infrastructure = ApiInfrastructure.using({ + * // With a database: + * ApiInfrastructure.using({ * mongoServer: { dbName, port, replSetName, seedData }, - * createApiServer: ({ getMongooseService }) => new ApolloGraphQLTestServer({ ... }), + * mongoose: { createService: (connectionString) => createMongooseService(connectionString) }, + * createGraphqlServer: ({ getMongooseService }) => new ApolloGraphQLTestServer({ ... }), + * }); + * + * // Without a database: + * ApiInfrastructure.using({ + * createGraphqlServer: () => new ApolloGraphQLTestServer({ ... }), * }); * ``` */ export class ApiInfrastructure { private readonly mongoServer: MongoMemoryTestServer | undefined; - private apiServer: TestServer | undefined; - private apiUrl: string | undefined; + private graphqlServer: TestServer | undefined; + private graphqlUrl: string | undefined; private mongooseService: TMongooseService | undefined; private shutdownHandlersRegistered = false; @@ -112,15 +121,15 @@ export class ApiInfrastructure(options: ApiInfrastructureOptions): ApiInfrastructure { return new ApiInfrastructure(options); } - /** Start MongoDB and the API server if they are not already running. */ + /** Start MongoDB (when configured) and the GraphQL server if they are not already running. */ async ensureStarted(): Promise { - if (this.apiServer?.isRunning()) { + if (this.graphqlServer?.isRunning()) { return; } @@ -130,26 +139,26 @@ export class ApiInfrastructure { if (this.mongoServer?.isRunning()) { await this.mongoServer.resetForScenario(); } } - /** Stop the API server and MongoDB, swallowing shutdown errors from already-failed resources. */ + /** Stop the GraphQL server, Mongoose service, and MongoDB, swallowing shutdown errors from already-failed resources. */ async stopAll(): Promise { - await this.apiServer?.stop().catch(() => undefined); - this.apiServer = undefined; + await this.graphqlServer?.stop().catch(() => undefined); + this.graphqlServer = undefined; if (this.mongooseService) { await Promise.resolve(this.mongooseService.shutDown()).catch(() => undefined); @@ -158,14 +167,14 @@ export class ApiInfrastructure undefined); - this.apiUrl = undefined; + this.graphqlUrl = undefined; } /** Return the current infrastructure state. */ getState(): ApiInfrastructureState { return { - apiUrl: this.apiUrl, - apiServer: this.apiServer, + graphqlUrl: this.graphqlUrl, + graphqlServer: this.graphqlServer, mongooseService: this.mongooseService, mongoServer: this.mongoServer, }; @@ -189,9 +198,9 @@ export class ApiInfrastructure mongoServer.getConnectionString() : () => { @@ -201,7 +210,7 @@ export class ApiInfrastructure { diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts index 907937c8e..eae26bdeb 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts @@ -1,207 +1,227 @@ import { type Browser, type BrowserContext, type BrowserContextOptions, chromium } from 'playwright'; import { BrowseTheWeb } from '../serenity/browse-the-web.ts'; -import { MongoMemoryTestServer, type MongoMemoryTestServerOptions } from '../servers/mongo-memory-test-server.ts'; import type { TestServer } from '../servers/test-server.ts'; -/** UI portal registration used by {@link E2EInfrastructure}. */ -export interface UiPortalRegistration { - /** Stable logical portal name, such as `community` or `staff`. */ - name: TName; - - /** Server that exposes the portal. */ - server: TestServer; -} - /** State exposed by {@link E2EInfrastructure}. */ export interface E2EInfrastructureState { - /** Running MongoDB server. */ - mongoServer: MongoMemoryTestServer | undefined; - - /** Running Azurite server. */ - azuriteServer: TestServer | undefined; + /** Every registered server, keyed by the name it was added with. */ + servers: Readonly>; - /** Running auth server. */ - authServer: TestServer | undefined; - - /** Running API server. */ - apiServer: TestServer | undefined; - - /** API URL resolved from the API server. */ - apiUrl: string | undefined; + /** Base URLs for every registered UI portal that has started. */ + uiPortalBaseUrls: Readonly>; - /** Browser launched for the suite. */ + /** Browser launched for the suite, when at least one UI portal is registered. */ browser: Browser | undefined; /** Browser ability assigned to Serenity actors. */ browseTheWeb: BrowseTheWeb | undefined; - - /** Base URLs for every registered UI portal. */ - uiPortalBaseUrls: Readonly>; } -/** Context supplied to E2E infrastructure callbacks. */ -export interface E2EInfrastructureCallbackContext { - /** Running MongoDB server, or `undefined` when none is configured. */ - mongoServer: MongoMemoryTestServer | undefined; - - /** Running Azurite server. */ - azuriteServer: TestServer; +/** Lookup passed to server factories so a server can read its already-started dependencies. */ +export interface E2EServerContext { + /** + * Resolve an already-started server by name. + * + * @typeParam TServer Concrete server type, for example `MongoMemoryTestServer`. + * @param name Name the dependency was registered with. + * @throws Error when the named server has not been created yet. Declare it in + * `dependsOn` so the framework starts it first. + */ + server(name: string): TServer; +} - /** Running auth server. */ - authServer: TestServer; +/** Factory that creates a server, optionally reading its dependencies from {@link E2EServerContext}. */ +export type E2EServerFactory = (context: E2EServerContext) => TestServer; - /** Running API server, when phase two has started. */ - apiServer?: TestServer; +/** Options accepted when registering a server with {@link E2EInfrastructure.addServer}. */ +export interface E2EServerOptions { + /** Names of servers that must be started before this one. */ + dependsOn?: string[]; - /** Resolve the MongoDB connection string after MongoDB has started. */ - getMongoConnectionString: () => string; + /** Reset this server between scenarios, e.g. clear and reseed a database. */ + resetForScenario?: (server: TestServer) => Promise | void; } -/** Factory that creates an API server once framework-owned MongoDB exists. */ -export type E2EApiServerFactory = (context: E2EInfrastructureCallbackContext) => TestServer; - -/** Factory used to create the framework-owned MongoDB server. */ -export type E2EMongoServerFactory = (options: MongoMemoryTestServerOptions) => MongoMemoryTestServer; +/** Options accepted when registering a UI portal with {@link E2EInfrastructure.addUiPortal}. */ +export interface E2EUiPortalOptions extends E2EServerOptions { + /** + * Playwright browser-context options used whenever this portal is browsed. + * `baseURL` defaults to the portal server's own URL, so navigation is scoped + * to this portal automatically. Merged over the suite-wide + * {@link E2EInfrastructureOptions.browserContextOptions}. + */ + contextOptions?: BrowserContextOptions; +} -/** Options used by {@link E2EInfrastructure.using}. */ +/** Options used by {@link E2EInfrastructure.create}. */ export interface E2EInfrastructureOptions { - /** MongoDB memory server options. Omit when the suite does not need MongoDB. */ - mongoServer?: MongoMemoryTestServerOptions; - - /** Optional MongoDB server factory. Defaults to `new MongoMemoryTestServer(options)`. */ - createMongoServer?: E2EMongoServerFactory; - - /** Azurite server required by the suite. */ - azuriteServer: TestServer; - - /** Auth server required by the suite. */ - authServer: TestServer; - - /** Factory that creates the API server with access to framework-owned MongoDB. */ - createApiServer: E2EApiServerFactory; - - /** Suite environment setup, such as starting a local proxy. */ + /** Suite environment setup, such as starting a local proxy. Runs before any server. */ setupEnvironment?: () => Promise | void; - /** Suite environment cleanup. */ + /** Suite environment cleanup. Runs after all servers stop. */ cleanupEnvironment?: () => Promise | void; /** Launch the browser in headless mode. Defaults to `true`. */ headless?: boolean; - /** Browser context options for the authenticated context. */ - browserContextOptions?: BrowserContextOptions | ((state: E2EInfrastructureState) => BrowserContextOptions); + /** + * Browser-context options shared by every portal context, such as + * `ignoreHTTPSErrors`. Each portal's own `baseURL` and `contextOptions` are + * merged over these, so this should not set a portal-specific `baseURL`. + */ + browserContextOptions?: BrowserContextOptions; +} + +interface ServerRegistration { + name: string; + factory: E2EServerFactory; + dependsOn: string[]; + resetForScenario?: (server: TestServer) => Promise | void; + contextOptions?: BrowserContextOptions; + isUiPortal: boolean; } /** * Lifecycle manager for browser E2E test suites. * - * The manager requires the base servers that every full-system verification - * suite needs: MongoDB, Azurite, auth, API, and at least one UI portal. Extra - * portals can be chained with {@link addUiPortal}, matching Cellix application - * startup APIs where the invariant pieces are required up front and optional - * pieces are registered fluently. + * Servers are composed fluently with {@link addServer} and {@link addUiPortal} + * instead of being fixed up front, so each consuming application registers only + * the servers it needs — a database here, an auth server there, one or many UI + * portals — and the framework owns startup ordering, scenario reset, browser + * setup, and shutdown. + * + * Servers start in dependency waves: every server whose `dependsOn` is satisfied + * starts in parallel, then the next wave, and so on. A factory receives an + * {@link E2EServerContext} so a dependent server (for example an API) can read a + * dependency's runtime state (for example a database connection string). The + * browser is launched only when at least one UI portal is registered, and each + * portal carries its own browser-context recipe — its `baseURL` is the portal's + * own URL — so {@link newPortalContext} opens a context for any portal without a + * caller naming the URL. The first registered portal backs the default + * `BrowseTheWeb` ability exposed on the state. * * @example * ```ts * export const infrastructure = E2EInfrastructure - * .using({ - * mongoServer: { dbName, port, replSetName, seedData }, - * azuriteServer, - * authServer, - * createApiServer: ({ getMongoConnectionString }) => createApiServer(getMongoConnectionString), + * .create({ browserContextOptions: { ignoreHTTPSErrors: true } }) + * .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { + * resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), * }) - * .addUiPortal('community', communityPortal) - * .addUiPortal('staff', staffPortal); + * .addServer('auth', () => createAuthServer()) + * .addServer('api', (ctx) => createApiServer(() => ctx.server('mongo').getConnectionString()), { + * dependsOn: ['mongo'], + * }) + * .addUiPortal('community', () => createCommunityPortal()) + * .addUiPortal('staff', () => createStaffPortal()); + * + * // Browse any portal — baseURL is that portal's own URL: + * const staffContext = await infrastructure.newPortalContext('staff'); * ``` */ export class E2EInfrastructure { - private readonly uiPortals = new Map(); - private readonly mongoServer: MongoMemoryTestServer | undefined; - private apiServer: TestServer | undefined; + private readonly registrations: ServerRegistration[] = []; + private readonly created = new Map(); + private readonly startOrder: string[] = []; + private readonly context: E2EServerContext = { + server: (name: string): TServer => { + const server = this.created.get(name); + if (!server) { + throw new Error(`E2EInfrastructure: server '${name}' is not available — declare it in dependsOn so it starts first`); + } + return server as TServer; + }, + }; + private environmentReady = false; - private apiUrl: string | undefined; private browser: Browser | undefined; private browserContext: BrowserContext | undefined; private browseTheWeb: BrowseTheWeb | undefined; private shutdownHandlersRegistered = false; - private constructor(private readonly options: E2EInfrastructureOptions) { - this.mongoServer = options.mongoServer ? (options.createMongoServer ?? ((mongoOptions) => new MongoMemoryTestServer(mongoOptions)))(options.mongoServer) : undefined; - } + private constructor(private readonly options: E2EInfrastructureOptions) {} /** * Create a browser E2E infrastructure manager. * - * @param options Required base servers and browser setup. + * @param options Browser and suite-environment setup. Defaults to an empty object. */ - static using(options: E2EInfrastructureOptions): E2EInfrastructure { + static create(options: E2EInfrastructureOptions = {}): E2EInfrastructure { return new E2EInfrastructure(options); } /** - * Register another UI portal server. + * Register a server. * - * @param name Stable logical portal name. - * @param server Portal server. + * @param name Unique server name, used by `dependsOn` and {@link E2EServerContext.server}. + * @param factory Creates the server, with access to already-started dependencies. + * @param options Dependencies and optional per-scenario reset. */ - addUiPortal(name: string, server: TestServer): this { - if (this.uiPortals.has(name)) { - throw new Error(`UI portal '${name}' is already registered`); - } + addServer(name: string, factory: E2EServerFactory, options: E2EServerOptions = {}): this { + return this.register(name, factory, options, false); + } - this.uiPortals.set(name, server); - return this; + /** + * Register a UI portal server. Portals contribute a base URL to + * {@link E2EInfrastructureState.uiPortalBaseUrls} and gate browser startup. + * + * @param name Stable logical portal name, such as `community` or `staff`. + * @param factory Creates the portal server. + * @param options Dependencies, per-scenario reset, and portal-scoped browser-context options. + */ + addUiPortal(name: string, factory: E2EServerFactory, options: E2EUiPortalOptions = {}): this { + return this.register(name, factory, options, true); + } + + /** + * Open a fresh browser context scoped to a UI portal. `baseURL` is the + * portal's own URL, merged with the suite-wide and portal-specific context + * options, so callers navigate relative paths without naming the portal URL. + * + * @param name Name of a registered, started UI portal. + */ + async newPortalContext(name: string): Promise { + const browser = await this.ensureBrowser(); + return browser.newContext(this.portalContextOptions(name)); } - /** Start the environment, base servers, UI portals, browser, and browser ability. */ + /** Start the environment, all servers (in dependency order), and the browser ability. */ async ensureStarted(): Promise { - if (this.uiPortals.size === 0) { - throw new Error('E2EInfrastructure requires at least one UI portal'); - } + this.assertDependenciesResolvable(); try { await this.ensureEnvironment(); - await this.startFoundationServers(); - await this.startApplicationServers(); - await this.ensureBrowserAbility(); + await this.startServers(); + if (this.hasUiPortal()) { + await this.ensureBrowserAbility(); + } } catch (error) { await this.stopAll(); throw error; } } - /** Reset mutable scenario state without restarting servers or the browser. */ + /** Reset mutable scenario state for every server that opted into a per-scenario reset. */ async resetScenarioState(): Promise { - if (this.mongoServer?.isRunning()) { - await this.mongoServer.resetForScenario(); + for (const registration of this.registrations) { + const server = this.created.get(registration.name); + if (registration.resetForScenario && server?.isRunning()) { + await registration.resetForScenario(server); + } } } - /** Stop browser resources, UI portals, base servers, and suite environment. */ + /** Stop browser resources, every server (in reverse start order), and the suite environment. */ async stopAll(): Promise { - if (this.browseTheWeb) { - await this.browseTheWeb.close().catch(() => undefined); - this.browseTheWeb = undefined; - this.browserContext = undefined; - } else if (this.browserContext) { - await this.browserContext.close().catch(() => undefined); - this.browserContext = undefined; - } + await this.closeBrowser(); - if (this.browser) { - await this.browser.close().catch(() => undefined); - this.browser = undefined; + for (const name of [...this.startOrder].reverse()) { + await this.created + .get(name) + ?.stop() + .catch(() => undefined); } - - await Promise.all([...this.uiPortals.values()].reverse().map((server) => server.stop().catch(() => undefined))); - await this.apiServer?.stop().catch(() => undefined); - await this.options.authServer.stop().catch(() => undefined); - await this.mongoServer?.stop().catch(() => undefined); - await this.options.azuriteServer.stop().catch(() => undefined); - - this.apiUrl = undefined; - this.apiServer = undefined; + this.created.clear(); + this.startOrder.length = 0; if (this.environmentReady) { await this.options.cleanupEnvironment?.(); @@ -211,17 +231,19 @@ export class E2EInfrastructure { /** Return the current infrastructure state. */ getState(): E2EInfrastructureState { - const uiPortalBaseUrls = Object.fromEntries([...this.uiPortals].map(([name, server]) => [name, server.getUrl()])); + const uiPortalBaseUrls: Record = {}; + for (const registration of this.registrations) { + const server = this.created.get(registration.name); + if (registration.isUiPortal && server) { + uiPortalBaseUrls[registration.name] = server.getUrl(); + } + } return { - apiServer: this.apiServer, - apiUrl: this.apiUrl, - authServer: this.options.authServer, - browseTheWeb: this.browseTheWeb, - browser: this.browser, - mongoServer: this.mongoServer, - azuriteServer: this.options.azuriteServer, + servers: Object.fromEntries(this.created), uiPortalBaseUrls, + browser: this.browser, + browseTheWeb: this.browseTheWeb, }; } @@ -243,6 +265,37 @@ export class E2EInfrastructure { return this; } + private register(name: string, factory: E2EServerFactory, options: E2EUiPortalOptions, isUiPortal: boolean): this { + if (this.registrations.some((registration) => registration.name === name)) { + throw new Error(`E2EInfrastructure: server '${name}' is already registered`); + } + + this.registrations.push({ + name, + factory, + dependsOn: options.dependsOn ?? [], + ...(options.resetForScenario && { resetForScenario: options.resetForScenario }), + ...(options.contextOptions && { contextOptions: options.contextOptions }), + isUiPortal, + }); + return this; + } + + private hasUiPortal(): boolean { + return this.registrations.some((registration) => registration.isUiPortal); + } + + private assertDependenciesResolvable(): void { + const names = new Set(this.registrations.map((registration) => registration.name)); + for (const registration of this.registrations) { + for (const dependency of registration.dependsOn) { + if (!names.has(dependency)) { + throw new Error(`E2EInfrastructure: server '${registration.name}' depends on unknown server '${dependency}'`); + } + } + } + } + private async ensureEnvironment(): Promise { if (this.environmentReady) { return; @@ -252,49 +305,85 @@ export class E2EInfrastructure { this.environmentReady = true; } - private async startFoundationServers(): Promise { - await Promise.all([ - this.mongoServer && !this.mongoServer.isRunning() ? this.mongoServer.start() : undefined, - this.options.azuriteServer.isRunning() ? undefined : this.options.azuriteServer.start(), - this.options.authServer.isRunning() ? undefined : this.options.authServer.start(), - ]); + private async startServers(): Promise { + const started = new Set(this.startOrder); + let remaining = this.registrations.filter((registration) => !started.has(registration.name)); + + while (remaining.length > 0) { + const wave = remaining.filter((registration) => registration.dependsOn.every((dependency) => started.has(dependency))); + if (wave.length === 0) { + throw new Error(`E2EInfrastructure: circular or unresolved dependencies among ${remaining.map((registration) => registration.name).join(', ')}`); + } + + await Promise.all(wave.map((registration) => this.startServer(registration))); + + for (const registration of wave) { + started.add(registration.name); + } + remaining = remaining.filter((registration) => !started.has(registration.name)); + } + } + + private async startServer(registration: ServerRegistration): Promise { + let server = this.created.get(registration.name); + if (!server) { + server = registration.factory(this.context); + this.created.set(registration.name, server); + } + + if (!server.isRunning()) { + await server.start(); + } + + if (!this.startOrder.includes(registration.name)) { + this.startOrder.push(registration.name); + } } - private async startApplicationServers(): Promise { - const apiServer = this.ensureApiServer(); - await Promise.all([apiServer.isRunning() ? undefined : apiServer.start(), ...[...this.uiPortals.values()].map((server) => (server.isRunning() ? undefined : server.start()))]); + private async closeBrowser(): Promise { + if (this.browseTheWeb) { + await this.browseTheWeb.close().catch(() => undefined); + this.browseTheWeb = undefined; + this.browserContext = undefined; + } else if (this.browserContext) { + await this.browserContext.close().catch(() => undefined); + this.browserContext = undefined; + } + + if (this.browser) { + await this.browser.close().catch(() => undefined); + this.browser = undefined; + } + } - this.apiUrl = apiServer.getUrl(); + private async ensureBrowser(): Promise { + this.browser ??= await chromium.launch({ headless: this.options.headless ?? true }); + return this.browser; } - private ensureApiServer(): TestServer { - const { mongoServer } = this; - this.apiServer ??= this.options.createApiServer({ - authServer: this.options.authServer, - azuriteServer: this.options.azuriteServer, - getMongoConnectionString: mongoServer - ? () => mongoServer.getConnectionString() - : () => { - throw new Error('E2EInfrastructure: no mongoServer configured'); - }, - mongoServer, - }); + private portalContextOptions(name: string): BrowserContextOptions { + const server = this.created.get(name); + const registration = this.registrations.find((entry) => entry.name === name && entry.isUiPortal); + if (!server || !registration) { + throw new Error(`E2EInfrastructure: UI portal '${name}' is not a started portal`); + } - return this.apiServer; + return { baseURL: server.getUrl(), ...this.options.browserContextOptions, ...registration.contextOptions }; } private async ensureBrowserAbility(): Promise { - if (!this.browser) { - this.browser = await chromium.launch({ headless: this.options.headless ?? true }); - } + await this.ensureBrowser(); if (this.browseTheWeb) { return; } - const state = this.getState(); - const contextOptions = typeof this.options.browserContextOptions === 'function' ? this.options.browserContextOptions(state) : this.options.browserContextOptions; - this.browserContext = await this.browser.newContext(contextOptions); + const primaryPortal = this.registrations.find((registration) => registration.isUiPortal); + if (!primaryPortal) { + return; + } + + this.browserContext = await this.newPortalContext(primaryPortal.name); const page = await this.browserContext.newPage(); try { diff --git a/packages/cellix/serenity-framework/src/infrastructure/index.ts b/packages/cellix/serenity-framework/src/infrastructure/index.ts index 5334f34c0..898ed9e0e 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/index.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/index.ts @@ -3,18 +3,11 @@ export type { ApiInfrastructureOptions, ApiInfrastructureState, ApiMongoServerFactory, - ApiServerFactory, - ApiServerFactoryContext, + GraphqlServerFactory, + GraphqlServerFactoryContext, ManagedMongooseConnection, ManagedMongooseService, } from './api-infrastructure.ts'; export { ApiInfrastructure } from './api-infrastructure.ts'; -export type { - E2EApiServerFactory, - E2EInfrastructureCallbackContext, - E2EInfrastructureOptions, - E2EInfrastructureState, - E2EMongoServerFactory, - UiPortalRegistration, -} from './e2e-infrastructure.ts'; +export type { E2EInfrastructureOptions, E2EInfrastructureState, E2EServerContext, E2EServerFactory, E2EServerOptions, E2EUiPortalOptions } from './e2e-infrastructure.ts'; export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts index ad428015e..d0361b2ae 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts @@ -73,19 +73,32 @@ function browserStubs() { } describe('ApiInfrastructure', () => { - it('starts MongoDB before the API server and exposes the API URL', async () => { + it('starts MongoDB before the GraphQL server and exposes the GraphQL URL', async () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); const infrastructure = ApiInfrastructure.using({ - createApiServer: () => graphQL, + createGraphqlServer: () => graphQL, createMongoServer: () => mongo, mongoServer: mongoOptions(), }); await infrastructure.ensureStarted(); - expect(infrastructure.getState().apiUrl).toBe('http://127.0.0.1:4000/graphql'); + expect(infrastructure.getState().graphqlUrl).toBe('http://127.0.0.1:4000/graphql'); + expect(graphQL.startCalls).toBe(1); + }); + + it('runs the GraphQL server without a database when no mongoServer is configured', async () => { + const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); + + const infrastructure = ApiInfrastructure.using({ createGraphqlServer: () => graphQL }); + + await infrastructure.ensureStarted(); + await infrastructure.resetScenarioState(); + + expect(infrastructure.getState().graphqlUrl).toBe('http://127.0.0.1:4000/graphql'); + expect(infrastructure.getState().mongoServer).toBeUndefined(); expect(graphQL.startCalls).toBe(1); }); @@ -93,7 +106,7 @@ describe('ApiInfrastructure', () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); const infrastructure = ApiInfrastructure.using({ - createApiServer: () => graphQL, + createGraphqlServer: () => graphQL, createMongoServer: () => mongo, mongoServer: mongoOptions(), }); @@ -108,7 +121,7 @@ describe('ApiInfrastructure', () => { }); describe('E2EInfrastructure', () => { - it('requires at least one chained UI portal and exposes all portal URLs', async () => { + it('starts servers in dependency order, wires the factory context, and exposes all portal URLs', async () => { const mongo = mongoServer(); const azurite = new FakeServer('http://127.0.0.1:10000'); const auth = new FakeServer('https://auth.test'); @@ -118,18 +131,27 @@ describe('E2EInfrastructure', () => { const { browser } = browserStubs(); vi.mocked(chromium.launch).mockResolvedValue(browser); - const infrastructure = E2EInfrastructure.using({ - authServer: auth, - azuriteServer: azurite, - createApiServer: () => api, - createMongoServer: () => mongo, - mongoServer: mongoOptions(), - }) - .addUiPortal('community', community) - .addUiPortal('staff', staff); + let apiSawMongoUrl: string | undefined; + const infrastructure = E2EInfrastructure.create() + .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) + .addServer('azurite', () => azurite) + .addServer('auth', () => auth) + .addServer( + 'api', + (ctx) => { + apiSawMongoUrl = ctx.server('mongo').getUrl(); + return api; + }, + { dependsOn: ['mongo'] }, + ) + .addUiPortal('community', () => community) + .addUiPortal('staff', () => staff); await infrastructure.ensureStarted(); + await infrastructure.resetScenarioState(); + expect(apiSawMongoUrl).toBe('mongodb://test'); + expect(mongo.resetCalls).toBe(1); expect(infrastructure.getState().uiPortalBaseUrls).toEqual({ community: 'https://community.test', staff: 'https://staff.test', @@ -138,18 +160,16 @@ describe('E2EInfrastructure', () => { expect(staff.startCalls).toBe(1); }); - it('creates the browser ability without owning app login behavior', async () => { - const mongo = mongoServer(); + it('creates the browser ability for the registered server set, without an app-specific shape', async () => { const { browser } = browserStubs(); vi.mocked(chromium.launch).mockResolvedValue(browser); - const infrastructure = E2EInfrastructure.using({ - authServer: new FakeServer('https://auth.test'), - azuriteServer: new FakeServer('http://127.0.0.1:10000'), - createApiServer: () => new FakeServer('https://api.test/api/graphql'), - createMongoServer: () => mongo, - mongoServer: mongoOptions(), - }).addUiPortal('community', new FakeServer('https://community.test')); + // Deliberately a leaner set than the suite above: no Azurite server. + const infrastructure = E2EInfrastructure.create() + .addServer('mongo', () => mongoServer()) + .addServer('auth', () => new FakeServer('https://auth.test')) + .addServer('api', () => new FakeServer('https://api.test/api/graphql'), { dependsOn: ['mongo'] }) + .addUiPortal('community', () => new FakeServer('https://community.test')); await infrastructure.ensureStarted(); diff --git a/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..66f7c3462 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts @@ -0,0 +1,28 @@ +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import type { IWorld } from '@cucumber/cucumber'; +import { isAgent } from 'std-env'; +import * as infra from './infrastructure.ts'; +import type { CellixApiWorld } from './world.ts'; + +let printedSuiteHeader = false; + +/** Register the Cucumber Before/After/AfterAll hooks for the API acceptance suite. */ +export function registerLifecycleHooks(): void { + registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + before: async (world) => { + if (!printedSuiteHeader && !isAgent) { + printedSuiteHeader = true; + console.log('\nAPI acceptance tests'); + console.log(' - Community context\n'); + } + + await world.init(); + }, + after: async (world) => { + await world.cleanup(); + }, + afterAll: () => infra.stopAll(), + }); +} diff --git a/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts b/packages/ocom-verification/acceptance-api/src/infrastructure.ts similarity index 85% rename from packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts rename to packages/ocom-verification/acceptance-api/src/infrastructure.ts index 117fe20f1..24b2c01b3 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/shared-infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/infrastructure.ts @@ -1,15 +1,16 @@ import { ApiInfrastructure, type ApiInfrastructureState } from '@cellix/serenity-framework/infrastructure/api'; import type { ServiceMongoose } from '@ocom/service-mongoose'; +import { getMongoPort } from '@ocom-verification/verification-shared/environment'; import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; import { createApiGraphQLServer, createApiMongooseService, resetApiGraphQLServerFactories } from './test-server-factories.ts'; const apiDbName = 'owner-community'; const infrastructure = ApiInfrastructure.using({ - createApiServer: ({ getMongooseService }) => createApiGraphQLServer(getMongooseService), + createGraphqlServer: ({ getMongooseService }) => createApiGraphQLServer(getMongooseService), mongoServer: { dbName: apiDbName, - port: 50_000, + port: getMongoPort(), replSetName: 'globaldb', seedData: seedDatabase, }, diff --git a/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts deleted file mode 100644 index e25861501..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/cucumber-lifecycle-hooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; -import { getTimeout } from '@cellix/serenity-framework/settings'; -import type { IWorld } from '@cucumber/cucumber'; -import { isAgent } from 'std-env'; -import { type CellixApiWorld, stopSharedServers } from '../world.ts'; - -let printedSuiteHeader = false; - -registerWorldLifecycleHooks({ - scenarioTimeout: getTimeout('scenario'), - before: async (world) => { - if (!printedSuiteHeader && !isAgent) { - printedSuiteHeader = true; - console.log('\nAPI acceptance tests'); - console.log(' - Community context\n'); - } - - await world.init(); - }, - after: async (world) => { - await world.cleanup(); - }, - afterAll: stopSharedServers, -}); diff --git a/packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts b/packages/ocom-verification/acceptance-api/src/test-server-factories.ts similarity index 94% rename from packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts rename to packages/ocom-verification/acceptance-api/src/test-server-factories.ts index 02c8ed7d7..1e23061c8 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/test-server-factories.ts +++ b/packages/ocom-verification/acceptance-api/src/test-server-factories.ts @@ -4,7 +4,7 @@ import { combinedSchema } from '@ocom/graphql'; import { ServiceMongoose } from '@ocom/service-mongoose'; import depthLimit from 'graphql-depth-limit'; import { applyMiddleware } from 'graphql-middleware'; -import { createMockApplicationServicesFactory } from './application-services/index.ts'; +import { createMockApplicationServicesFactory } from './shared/application-services/index.ts'; let mockApplicationServicesFactory: ReturnType | undefined; diff --git a/packages/ocom-verification/acceptance-api/src/world.ts b/packages/ocom-verification/acceptance-api/src/world.ts index 3c0adb7a8..f1fed41d3 100644 --- a/packages/ocom-verification/acceptance-api/src/world.ts +++ b/packages/ocom-verification/acceptance-api/src/world.ts @@ -1,13 +1,9 @@ import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; +import { registerLifecycleHooks } from './cucumber-lifecycle-hooks.ts'; +import * as infra from './infrastructure.ts'; import { createCommunityAbility } from './shared/abilities/create-community.ts'; import { createGraphQLClientAbility } from './shared/abilities/graphql-client.ts'; -import './shared/cucumber-lifecycle-hooks.ts'; -import * as infra from './shared/shared-infrastructure.ts'; - -export async function stopSharedServers(): Promise { - await infra.stopAll(); -} export const CellixApiWorld = registerManagedSerenityWorld({ infrastructure: { @@ -17,15 +13,17 @@ export const CellixApiWorld = registerManagedSerenityWorld({ stopAll: infra.stopAll, }, validateState: (state) => { - if (!state.apiUrl) { - throw new Error('API acceptance infrastructure did not expose an apiUrl'); + if (!state.graphqlUrl) { + throw new Error('API acceptance infrastructure did not expose a graphqlUrl'); } }, createCast: (state) => new SerenityCast({ useNotepad: true, - abilities: [() => createGraphQLClientAbility(state.apiUrl ?? ''), () => createCommunityAbility()], + abilities: [() => createGraphQLClientAbility(state.graphqlUrl ?? ''), () => createCommunityAbility()], }), }); export type CellixApiWorld = InstanceType; + +registerLifecycleHooks(); diff --git a/packages/ocom-verification/acceptance-ui/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-ui/src/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..4c8e10126 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/cucumber-lifecycle-hooks.ts @@ -0,0 +1,25 @@ +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import { After } from '@cucumber/cucumber'; +import { actorInTheSpotlight } from '@serenity-js/core'; +import type { CellixUiWorld } from './world.ts'; + +/** Register the Cucumber Before/After hooks for the component acceptance suite. */ +export function registerLifecycleHooks(): void { + registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + beforeTimeout: getTimeout('uiInit'), + before: async (world) => { + await world.init(); + }, + }); + + After({ timeout: getTimeout('uiCleanup') }, () => { + try { + RenderInDom.as(actorInTheSpotlight()).unmount(); + } catch { + /* No component was rendered in this scenario. */ + } + }); +} diff --git a/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts deleted file mode 100644 index d45035281..000000000 --- a/packages/ocom-verification/acceptance-ui/src/shared/cucumber-lifecycle-hooks.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; -import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; -import { getTimeout } from '@cellix/serenity-framework/settings'; -import { After } from '@cucumber/cucumber'; -import { actorInTheSpotlight } from '@serenity-js/core'; -import type { CellixUiWorld } from '../world.ts'; - -registerWorldLifecycleHooks({ - scenarioTimeout: getTimeout('scenario'), - beforeTimeout: getTimeout('uiInit'), - before: async (world) => { - await world.init(); - }, -}); - -After({ timeout: getTimeout('uiCleanup') }, () => { - try { - RenderInDom.as(actorInTheSpotlight()).unmount(); - } catch { - /* No component was rendered in this scenario. */ - } -}); diff --git a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts index 1b7c8d7b3..395b3f752 100644 --- a/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts +++ b/packages/ocom-verification/acceptance-ui/src/step-definitions/index.ts @@ -3,6 +3,5 @@ * Cucumber imports this file, which then loads all context-specific step definitions. */ -import '../shared/cucumber-lifecycle-hooks.ts'; import '../contexts/community/step-definitions/index.ts'; import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/acceptance-ui/src/world.ts b/packages/ocom-verification/acceptance-ui/src/world.ts index 747d7249f..df5d9648f 100644 --- a/packages/ocom-verification/acceptance-ui/src/world.ts +++ b/packages/ocom-verification/acceptance-ui/src/world.ts @@ -1,6 +1,7 @@ import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; import { RenderInDom } from '@cellix/serenity-framework/dom/render-in-dom'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; +import { registerLifecycleHooks } from './cucumber-lifecycle-hooks.ts'; export const CellixUiWorld = registerManagedSerenityWorld>({ infrastructure: { @@ -15,3 +16,5 @@ export const CellixUiWorld = registerManagedSerenityWorld> }); export type CellixUiWorld = InstanceType; + +registerLifecycleHooks(); diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index 7f3035b8b..c5860ec97 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,7 +1,7 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; import type { BrowserContext, Page } from 'playwright'; -import * as infra from '../../../shared/shared-infrastructure.ts'; +import * as infra from '../../../infrastructure.ts'; import type { CellixE2EWorld } from '../../../world.ts'; import type { HeaderE2ENotes, HeaderE2ESite } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; @@ -62,17 +62,10 @@ When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorNam s.actorName = actorName; const actor = actorCalled(actorName); - const { browser } = infra.getState(); - if (!browser) throw new Error('Browser not launched'); - - const baseUrl = s.site === 'community' ? (infra.getUiBaseUrl('community') ?? 'https://ownercommunity.localhost:1355') : (infra.getUiBaseUrl('staff') ?? 'https://staff.ownercommunity.localhost:1355'); - - // Fresh unauthenticated context — isolated from the pre-auth context - // used by other test suites. Cleaned up in the Then step after verification. - const context = await browser.newContext({ - baseURL: baseUrl, - ignoreHTTPSErrors: true, - }); + // Fresh unauthenticated context for the portal under test — its baseURL is the + // portal's own URL. Isolated from the pre-auth context used by other suites; + // cleaned up in the Then step after verification. + const context = await infra.newPortalContext(s.site); s.context = context; if (s.identityProviderUnreachable) { diff --git a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..511fc56e0 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts @@ -0,0 +1,28 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; +import { registerScreenshotOnFailureHook } from '@cellix/serenity-framework/cucumber/screenshot'; +import { getTimeout } from '@cellix/serenity-framework/settings'; +import type { IWorld } from '@cucumber/cucumber'; +import * as infra from './infrastructure.ts'; +import type { CellixE2EWorld } from './world.ts'; + +const currentDir = fileURLToPath(new URL('.', import.meta.url)); + +/** Register the Cucumber Before/After/AfterAll and screenshot hooks for the E2E suite. */ +export function registerLifecycleHooks(): void { + registerWorldLifecycleHooks({ + scenarioTimeout: getTimeout('scenario'), + before: async (world) => { + await world.init(); + }, + after: async (world) => { + await world.cleanup(); + }, + afterAll: () => infra.stopAll(), + }); + + registerScreenshotOnFailureHook({ + reportsDir: path.resolve(currentDir, '..', '..', 'reports', 'screenshots'), + }); +} diff --git a/packages/ocom-verification/e2e-tests/src/infrastructure.ts b/packages/ocom-verification/e2e-tests/src/infrastructure.ts new file mode 100644 index 000000000..89d401faa --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/infrastructure.ts @@ -0,0 +1,44 @@ +import { E2EInfrastructure, type E2EInfrastructureState } from '@cellix/serenity-framework/infrastructure/e2e'; +import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; +import { getMongoPort } from '@ocom-verification/verification-shared/environment'; +import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; +import type { BrowserContext } from 'playwright'; +import { cleanupTestEnvironment, createCommunityUiPortalServer, createStaffUiPortalServer, createTestApiServer, createTestAzuriteServer, createTestOAuth2Server, initTestEnvironment } from './test-server-factories.ts'; + +const apiDbName = 'owner-community'; + +const infrastructure = E2EInfrastructure.create({ + // baseURL is supplied per portal by the framework; only shared options here. + browserContextOptions: { ignoreHTTPSErrors: true }, + cleanupEnvironment: cleanupTestEnvironment, + setupEnvironment: initTestEnvironment, +}) + .addServer('mongo', () => new MongoMemoryTestServer({ dbName: apiDbName, port: getMongoPort(), replSetName: 'globaldb', seedData: seedDatabase }), { + resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), + }) + .addServer('azurite', () => createTestAzuriteServer()) + .addServer('auth', () => createTestOAuth2Server()) + .addServer('api', (ctx) => createTestApiServer(() => ctx.server('mongo').getConnectionString()), { dependsOn: ['mongo'] }) + .addUiPortal('community', () => createCommunityUiPortalServer()) + .addUiPortal('staff', () => createStaffUiPortalServer()); + +export function getState(): E2EInfrastructureState { + return infrastructure.getState(); +} + +export function newPortalContext(site: 'community' | 'staff'): Promise { + return infrastructure.newPortalContext(site); +} + +export async function resetScenarioState(): Promise { + await infrastructure.resetScenarioState(); +} + +export async function stopAll(): Promise { + await infrastructure.stopAll(); +} + +export async function ensureE2EServers(): Promise { + infrastructure.registerProcessShutdownHandlers(); + await infrastructure.ensureStarted(); +} diff --git a/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts deleted file mode 100644 index 89b7424b3..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/cucumber-lifecycle-hooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber'; -import { registerScreenshotOnFailureHook } from '@cellix/serenity-framework/cucumber/screenshot'; -import { getTimeout } from '@cellix/serenity-framework/settings'; -import type { IWorld } from '@cucumber/cucumber'; -import { type CellixE2EWorld, stopSharedServers } from '../world.ts'; - -const currentDir = fileURLToPath(new URL('.', import.meta.url)); - -registerWorldLifecycleHooks({ - scenarioTimeout: getTimeout('scenario'), - before: async (world) => { - await world.init(); - }, - after: async (world) => { - await world.cleanup(); - }, - afterAll: stopSharedServers, -}); - -registerScreenshotOnFailureHook({ - reportsDir: path.resolve(currentDir, '..', '..', '..', 'reports', 'screenshots'), -}); diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts deleted file mode 100644 index 022abad6b..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/environment/worktree-ports.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface AzuritePorts { - blob: number; - queue: number; - table: number; -} - -export function getWorktreePortOffset(): number { - const name = process.env['WORKTREE_NAME']; - if (!name) return 0; - let hash = 0; - for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; - return ((Math.abs(hash) % 49) + 1) * 100; -} - -export function getAzuritePorts(): AzuritePorts { - const offset = getWorktreePortOffset(); - return { - blob: 10000 + offset, - queue: 10001 + offset, - table: 10002 + offset, - }; -} - -export function getMongoPort(): number { - return 50000 + getWorktreePortOffset(); -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts b/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts deleted file mode 100644 index 2fbdc6d91..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/shared-infrastructure.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { E2EInfrastructure, type E2EInfrastructureState } from '@cellix/serenity-framework/infrastructure/e2e'; -import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; -import { getMongoPort } from './environment/worktree-ports.ts'; -import { cleanupTestEnvironment, createCommunityUiPortalServer, createStaffUiPortalServer, createTestApiServer, createTestAzuriteServer, createTestOAuth2Server, initTestEnvironment } from './test-server-factories.ts'; - -const apiDbName = 'owner-community'; - -const infrastructure = E2EInfrastructure.using({ - authServer: createTestOAuth2Server(), - azuriteServer: createTestAzuriteServer(), - browserContextOptions: (state) => { - const communityBaseUrl = state.uiPortalBaseUrls['community']; - if (!communityBaseUrl) { - throw new Error('Community UI portal URL was not initialized'); - } - - return { - baseURL: communityBaseUrl, - ignoreHTTPSErrors: true, - }; - }, - cleanupEnvironment: cleanupTestEnvironment, - createApiServer: ({ getMongoConnectionString }) => createTestApiServer(getMongoConnectionString), - mongoServer: { - dbName: apiDbName, - port: getMongoPort(), - replSetName: 'globaldb', - seedData: seedDatabase, - }, - setupEnvironment: initTestEnvironment, -}) - .addUiPortal('community', createCommunityUiPortalServer()) - .addUiPortal('staff', createStaffUiPortalServer()); - -export function getState(): E2EInfrastructureState { - return infrastructure.getState(); -} - -export function getUiBaseUrl(site: 'community' | 'staff'): string | undefined { - return infrastructure.getState().uiPortalBaseUrls[site]; -} - -export async function resetScenarioState(): Promise { - await infrastructure.resetScenarioState(); -} - -export async function stopAll(): Promise { - await infrastructure.stopAll(); -} - -export async function ensureE2EServers(): Promise { - infrastructure.registerProcessShutdownHandlers(); - await infrastructure.ensureStarted(); -} diff --git a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts index 1b7c8d7b3..395b3f752 100644 --- a/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts +++ b/packages/ocom-verification/e2e-tests/src/step-definitions/index.ts @@ -3,6 +3,5 @@ * Cucumber imports this file, which then loads all context-specific step definitions. */ -import '../shared/cucumber-lifecycle-hooks.ts'; import '../contexts/community/step-definitions/index.ts'; import '../contexts/authentication/step-definitions/index.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts b/packages/ocom-verification/e2e-tests/src/test-server-factories.ts similarity index 84% rename from packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts rename to packages/ocom-verification/e2e-tests/src/test-server-factories.ts index d8acc69ab..83d72cb67 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/test-server-factories.ts +++ b/packages/ocom-verification/e2e-tests/src/test-server-factories.ts @@ -1,13 +1,13 @@ import { join } from 'node:path'; import { ProcessTestServer } from '@cellix/serenity-framework/servers'; -import { appPaths } from './environment/app-paths.ts'; -import { e2eEnv, getPortlessDevScript } from './environment/dev-script.ts'; -import { buildUrl, cleanupTestEnvironment, getHostnames, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer } from './environment/test-environment.ts'; -import { getAzuritePorts } from './environment/worktree-ports.ts'; +import { getAzuritePorts } from '@ocom-verification/verification-shared/environment'; +import { appPaths } from './shared/environment/app-paths.ts'; +import { e2eEnv, getPortlessDevScript } from './shared/environment/dev-script.ts'; +import { buildUrl, getHostnames, mockOidcEndpoint, mockOidcIssuer } from './shared/environment/test-environment.ts'; -const hostnames = getHostnames(); +export { cleanupTestEnvironment, initTestEnvironment } from './shared/environment/test-environment.ts'; -export { buildUrl, cleanupTestEnvironment, initTestEnvironment, mockOidcAudience, mockOidcEndpoint, mockOidcIssuer, mockStaffOidcIssuer }; +const hostnames = getHostnames(); export function createTestApiServer(getMongoConnectionString: () => string): ProcessTestServer { return new ProcessTestServer({ diff --git a/packages/ocom-verification/e2e-tests/src/world.ts b/packages/ocom-verification/e2e-tests/src/world.ts index b9da6e5e8..2e5d6a44d 100644 --- a/packages/ocom-verification/e2e-tests/src/world.ts +++ b/packages/ocom-verification/e2e-tests/src/world.ts @@ -1,12 +1,8 @@ import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; -import './shared/cucumber-lifecycle-hooks.ts'; +import { registerLifecycleHooks } from './cucumber-lifecycle-hooks.ts'; +import * as infra from './infrastructure.ts'; import { OAuth2Login } from './shared/abilities/oauth2-login.ts'; -import * as infra from './shared/shared-infrastructure.ts'; - -export async function stopSharedServers(): Promise { - await infra.stopAll(); -} export const CellixE2EWorld = registerManagedSerenityWorld({ infrastructure: { @@ -36,3 +32,5 @@ export const CellixE2EWorld = registerManagedSerenityWorld({ }); export type CellixE2EWorld = InstanceType; + +registerLifecycleHooks(); diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index e135dfb0f..d2873cc32 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -6,6 +6,7 @@ "type": "module", "exports": { "./abilities": "./src/abilities/index.ts", + "./environment": "./src/environment/index.ts", "./pages": "./src/pages/index.ts", "./test-data": "./src/test-data/index.ts" }, diff --git a/packages/ocom-verification/verification-shared/src/environment/index.ts b/packages/ocom-verification/verification-shared/src/environment/index.ts new file mode 100644 index 000000000..c2ddae3ce --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/environment/index.ts @@ -0,0 +1 @@ +export { type AzuritePorts, getAzuritePorts, getMongoPort, getWorktreePortOffset } from './worktree-ports.ts'; diff --git a/packages/ocom-verification/verification-shared/src/environment/worktree-ports.ts b/packages/ocom-verification/verification-shared/src/environment/worktree-ports.ts new file mode 100644 index 000000000..b44b57e35 --- /dev/null +++ b/packages/ocom-verification/verification-shared/src/environment/worktree-ports.ts @@ -0,0 +1,39 @@ +/** Internal test-service ports used by a single verification worktree. */ +export interface AzuritePorts { + /** Azurite blob service port. */ + blob: number; + /** Azurite queue service port. */ + queue: number; + /** Azurite table service port. */ + table: number; +} + +/** + * Resolve the deterministic local-service port offset for the current worktree. + * + * When `WORKTREE_NAME` is absent, verification suites use the default local + * ports. When it is present, each worktree name maps to a stable offset so + * MongoDB and Azurite test servers can run beside another worktree. + */ +export function getWorktreePortOffset(): number { + const { WORKTREE_NAME: name } = process.env; + if (!name) return 0; + let hash = 0; + for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; + return ((Math.abs(hash) % 49) + 1) * 100; +} + +/** MongoDB memory-server port for the current verification worktree. */ +export function getMongoPort(): number { + return 50_000 + getWorktreePortOffset(); +} + +/** Azurite service ports for the current verification worktree. */ +export function getAzuritePorts(): AzuritePorts { + const offset = getWorktreePortOffset(); + return { + blob: 10_000 + offset, + queue: 10_001 + offset, + table: 10_002 + offset, + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d6ffe217..a0d68c5c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,8 +79,8 @@ catalogs: specifier: ^19.1.1 version: 19.2.0 react-router-dom: - specifier: 7.12.0 - version: 7.12.0 + specifier: 7.15.0 + version: 7.15.0 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -487,7 +487,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -617,7 +617,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -1084,7 +1084,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1888,7 +1888,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -1982,7 +1982,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -2134,7 +2134,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@ant-design/icons': specifier: 'catalog:' @@ -2216,7 +2216,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -2298,7 +2298,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2341,7 +2341,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2427,7 +2427,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2470,7 +2470,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2519,7 +2519,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -11664,8 +11664,8 @@ packages: peerDependencies: react: '>=15' - react-router-dom@7.12.0: - resolution: {integrity: sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==} + react-router-dom@7.15.0: + resolution: {integrity: sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -11676,8 +11676,8 @@ packages: peerDependencies: react: '>=15' - react-router@7.12.0: - resolution: {integrity: sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==} + react-router@7.15.0: + resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -24899,11 +24899,11 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router-dom@7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router-dom@7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - react-router: 7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-router@5.3.4(react@19.2.0): dependencies: @@ -24918,7 +24918,7 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router@7.12.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.1.1 react: 19.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9494ac83b..25940d5df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,7 +27,7 @@ catalog: jsdom: ^26.1.0 mongodb: 6.18.0 mongoose: 8.17.0 - react-router-dom: 7.12.0 + react-router-dom: 7.15.0 react: ^19.1.1 react-dom: ^19.1.1 '@ant-design/icons': ^6.0.2 From a44fbe92c43b514ad7afe2d32274b39317b6cca1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 3 Jun 2026 15:23:15 -0400 Subject: [PATCH 17/40] small refactor for test server pattern in api --- .../src/infrastructure/api-infrastructure.ts | 320 +++++++++--------- .../src/infrastructure/index.ts | 11 +- .../src/infrastructure/infrastructure.test.ts | 60 ++-- .../acceptance-api/src/infrastructure.ts | 40 ++- .../mock-application-services.ts | 0 .../src/shared/application-services/index.ts | 1 - .../src/test-server-factories.ts | 117 +++++-- 7 files changed, 308 insertions(+), 241 deletions(-) rename packages/ocom-verification/acceptance-api/src/{shared/application-services => }/mock-application-services.ts (100%) delete mode 100644 packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts index 8744c626c..397f4b512 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -1,182 +1,176 @@ -import { MongoMemoryTestServer, type MongoMemoryTestServerOptions } from '../servers/mongo-memory-test-server.ts'; import type { TestServer } from '../servers/test-server.ts'; -/** Minimal connection shape needed for model cleanup. */ -export interface ManagedMongooseConnection { - /** Registered Mongoose model map. */ - models: Record; +/** Suite-level environment hooks for {@link ApiInfrastructure}. */ +export interface ApiInfrastructureOptions { + /** Suite environment setup, such as starting a local proxy. Runs before any server. */ + setupEnvironment?: () => Promise | void; - /** Delete a registered Mongoose model by name. */ - deleteModel(name: string): unknown; + /** Suite environment cleanup. Runs after all servers stop. */ + cleanupEnvironment?: () => Promise | void; } -/** Minimal Mongoose service shape managed by API infrastructure. */ -export interface ManagedMongooseService { - /** Start the service. */ - startUp(): Promise | unknown; - - /** Stop the service. */ - shutDown(): Promise | unknown; - - /** Service internals exposing the active connection. */ - service: { - /** Active Mongoose connection. */ - connection: ManagedMongooseConnection; - }; -} - -/** Options used when API infrastructure owns a consumer-provided Mongoose service. */ -export interface ApiInfrastructureMongooseOptions { - /** Create the service from the framework-owned MongoDB connection string. */ - createService: (connectionString: string) => TMongooseService; - - /** Clear registered models after startup. Defaults to true. */ - clearModels?: boolean; +/** Lookup passed to server factories so a server can read its already-started dependencies. */ +export interface ApiServerContext { + /** + * Resolve an already-started server by name. + * + * @typeParam TServer Concrete server type, for example `MongoMemoryTestServer`. + * @param name Name the dependency was registered with. + * @throws Error when the named server has not been created yet. Declare it in + * `dependsOn` so the framework starts it first. + */ + server(name: string): TServer; } -/** Context supplied to the GraphQL server factory. */ -export interface GraphqlServerFactoryContext { - /** Framework-owned MongoDB server, or `undefined` when none is configured. */ - mongoServer: MongoMemoryTestServer | undefined; +/** Factory that creates a server, optionally reading its dependencies from {@link ApiServerContext}. */ +export type ApiServerFactory = (context: ApiServerContext) => TestServer; - /** Resolve the MongoDB connection string. Throws if no `mongoServer` is configured. */ - getMongoConnectionString: () => string; +/** Options accepted when registering a server with {@link ApiInfrastructure.addServer}. */ +export interface ApiServerOptions { + /** Names of servers that must be started before this one. */ + dependsOn?: string[]; - /** Resolve the managed Mongoose service. Throws if no `mongoose` option is configured. */ - getMongooseService: () => TMongooseService; + /** Reset this server between scenarios, e.g. clear and reseed a database. */ + resetForScenario?: (server: TestServer) => Promise | void; } -/** Factory that creates the GraphQL server after framework-owned resources are available. */ -export type GraphqlServerFactory = (context: GraphqlServerFactoryContext) => TestServer; - -/** Factory used to create the framework-owned MongoDB server. */ -export type ApiMongoServerFactory = (options: MongoMemoryTestServerOptions) => MongoMemoryTestServer; - /** State exposed by {@link ApiInfrastructure}. */ -export interface ApiInfrastructureState { - /** Running MongoDB server, or `undefined` when none is configured. */ - mongoServer: MongoMemoryTestServer | undefined; - - /** Running GraphQL server for the suite. */ - graphqlServer: TestServer | undefined; - - /** Running Mongoose service, or `undefined` when none is configured. */ - mongooseService: TMongooseService | undefined; - - /** GraphQL endpoint URL, when the server has started. */ - graphqlUrl: string | undefined; +export interface ApiInfrastructureState { + /** Every registered server, keyed by the name it was added with. */ + servers: Readonly>; } -/** Options used by {@link ApiInfrastructure.using}. */ -export interface ApiInfrastructureOptions { - /** MongoDB memory server options. Omit when the suite does not need a database. */ - mongoServer?: MongoMemoryTestServerOptions; - - /** Optional MongoDB server factory. Defaults to `new MongoMemoryTestServer(options)`. */ - createMongoServer?: ApiMongoServerFactory; - - /** Optional Mongoose service managed between MongoDB and the GraphQL server. Requires `mongoServer`. */ - mongoose?: ApiInfrastructureMongooseOptions; - - /** Factory that creates the GraphQL server with access to framework-owned resources. */ - createGraphqlServer: GraphqlServerFactory; +interface ServerRegistration { + name: string; + factory: ApiServerFactory; + dependsOn: string[]; + resetForScenario?: (server: TestServer) => Promise | void; } /** - * Lifecycle manager for API acceptance tests. + * Lifecycle manager for API acceptance test suites. + * + * Servers are composed fluently with {@link addServer} rather than being fixed up + * front, so each consuming suite registers only the servers it needs — a database + * here, an ORM connection there, a GraphQL server on top — and the framework owns + * startup ordering, scenario reset, and shutdown. The manager is ignorant of what + * each server is: a Mongo memory server, a SQL server, an Apollo GraphQL server, + * or anything else implementing {@link TestServer}. A suite with no database, a + * non-Mongo database, or no GraphQL layer simply registers a different server set. * - * The only always-present piece is a consumer-provided GraphQL server. MongoDB - * and a Mongoose service are both optional — omit them for an app with no - * database, or supply `mongoServer` (and optionally `mongoose`) when the GraphQL - * server needs persistence. Consumers configure the concrete server objects with - * app-specific schema, context, services, seed data, and environment values - * before passing factories to the framework. + * Servers start in dependency waves: every server whose `dependsOn` is satisfied + * starts in parallel, then the next wave, and so on. A factory receives an + * {@link ApiServerContext} so a dependent server (for example a GraphQL server) can + * read a dependency's runtime state (for example a database connection string). * * @example * ```ts - * // With a database: - * ApiInfrastructure.using({ - * mongoServer: { dbName, port, replSetName, seedData }, - * mongoose: { createService: (connectionString) => createMongooseService(connectionString) }, - * createGraphqlServer: ({ getMongooseService }) => new ApolloGraphQLTestServer({ ... }), - * }); + * // With a database and a GraphQL server: + * ApiInfrastructure.create() + * .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { + * resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), + * }) + * .addServer('graphql', (ctx) => createGraphqlServer(() => ctx.server('mongo').getUrl()), { + * dependsOn: ['mongo'], + * }); * * // Without a database: - * ApiInfrastructure.using({ - * createGraphqlServer: () => new ApolloGraphQLTestServer({ ... }), - * }); + * ApiInfrastructure.create().addServer('graphql', () => createGraphqlServer()); * ``` */ -export class ApiInfrastructure { - private readonly mongoServer: MongoMemoryTestServer | undefined; - private graphqlServer: TestServer | undefined; - private graphqlUrl: string | undefined; - private mongooseService: TMongooseService | undefined; +export class ApiInfrastructure { + private readonly registrations: ServerRegistration[] = []; + private readonly created = new Map(); + private readonly startOrder: string[] = []; + private readonly context: ApiServerContext = { + server: (name: string): TServer => { + const server = this.created.get(name); + if (!server) { + throw new Error(`ApiInfrastructure: server '${name}' is not available — declare it in dependsOn so it starts first`); + } + return server as TServer; + }, + }; + + private environmentReady = false; private shutdownHandlersRegistered = false; - private constructor(private readonly options: ApiInfrastructureOptions) { - this.mongoServer = options.mongoServer ? (options.createMongoServer ?? ((mongoOptions) => new MongoMemoryTestServer(mongoOptions)))(options.mongoServer) : undefined; - } + private constructor(private readonly options: ApiInfrastructureOptions) {} /** * Create an API acceptance infrastructure manager. * - * @param options A GraphQL server factory, plus optional MongoDB and Mongoose configuration. + * @param options Suite-environment setup. Defaults to an empty object. */ - static using(options: ApiInfrastructureOptions): ApiInfrastructure { + static create(options: ApiInfrastructureOptions = {}): ApiInfrastructure { return new ApiInfrastructure(options); } - /** Start MongoDB (when configured) and the GraphQL server if they are not already running. */ - async ensureStarted(): Promise { - if (this.graphqlServer?.isRunning()) { - return; + /** + * Register a server. + * + * @param name Unique server name, used by `dependsOn` and {@link ApiServerContext.server}. + * @param factory Creates the server, with access to already-started dependencies. + * @param options Dependencies and optional per-scenario reset. + */ + addServer(name: string, factory: ApiServerFactory, options: ApiServerOptions = {}): this { + if (this.registrations.some((registration) => registration.name === name)) { + throw new Error(`ApiInfrastructure: server '${name}' is already registered`); } - try { - if (this.mongoServer && !this.mongoServer.isRunning()) { - await this.mongoServer.start(); - } + this.registrations.push({ + name, + factory, + dependsOn: options.dependsOn ?? [], + ...(options.resetForScenario && { resetForScenario: options.resetForScenario }), + }); + return this; + } + + /** Start the environment and all servers (in dependency order) if they are not already running. */ + async ensureStarted(): Promise { + this.assertDependenciesResolvable(); - await this.ensureMongooseService(); - const graphqlServer = this.ensureGraphqlServer(); - await graphqlServer.start(); - this.graphqlUrl = graphqlServer.getUrl(); + try { + await this.ensureEnvironment(); + await this.startServers(); } catch (error) { await this.stopAll(); throw error; } } - /** Reset MongoDB between scenarios without restarting the GraphQL server. No-op when no database is configured. */ + /** Reset mutable scenario state for every running server that opted into a per-scenario reset. */ async resetScenarioState(): Promise { - if (this.mongoServer?.isRunning()) { - await this.mongoServer.resetForScenario(); + for (const registration of this.registrations) { + const server = this.created.get(registration.name); + if (registration.resetForScenario && server?.isRunning()) { + await registration.resetForScenario(server); + } } } - /** Stop the GraphQL server, Mongoose service, and MongoDB, swallowing shutdown errors from already-failed resources. */ + /** Stop every server (in reverse start order) and the suite environment, swallowing shutdown errors. */ async stopAll(): Promise { - await this.graphqlServer?.stop().catch(() => undefined); - this.graphqlServer = undefined; - - if (this.mongooseService) { - await Promise.resolve(this.mongooseService.shutDown()).catch(() => undefined); - this.mongooseService = undefined; + for (const name of [...this.startOrder].reverse()) { + await this.created + .get(name) + ?.stop() + .catch(() => undefined); } + this.created.clear(); + this.startOrder.length = 0; - await this.mongoServer?.stop().catch(() => undefined); - - this.graphqlUrl = undefined; + if (this.environmentReady) { + await this.options.cleanupEnvironment?.(); + this.environmentReady = false; + } } /** Return the current infrastructure state. */ - getState(): ApiInfrastructureState { + getState(): ApiInfrastructureState { return { - graphqlUrl: this.graphqlUrl, - graphqlServer: this.graphqlServer, - mongooseService: this.mongooseService, - mongoServer: this.mongoServer, + servers: Object.fromEntries(this.created), }; } @@ -198,56 +192,58 @@ export class ApiInfrastructure mongoServer.getConnectionString() - : () => { - throw new Error('ApiInfrastructure: no mongoServer configured'); - }, - getMongooseService: () => this.getMongooseService(), - mongoServer, - }); - - return this.graphqlServer; + private assertDependenciesResolvable(): void { + const names = new Set(this.registrations.map((registration) => registration.name)); + for (const registration of this.registrations) { + for (const dependency of registration.dependsOn) { + if (!names.has(dependency)) { + throw new Error(`ApiInfrastructure: server '${registration.name}' depends on unknown server '${dependency}'`); + } + } + } } - private async ensureMongooseService(): Promise { - if (!this.options.mongoose) { - return undefined; + private async ensureEnvironment(): Promise { + if (this.environmentReady) { + return; } - if (!this.mongoServer) { - throw new Error('ApiInfrastructure: mongoose option requires mongoServer to be configured'); - } + await this.options.setupEnvironment?.(); + this.environmentReady = true; + } + + private async startServers(): Promise { + const started = new Set(this.startOrder); + let remaining = this.registrations.filter((registration) => !started.has(registration.name)); - if (!this.mongooseService) { - this.mongooseService = this.options.mongoose.createService(this.mongoServer.getConnectionString()); - await this.mongooseService.startUp(); - if (this.options.mongoose.clearModels ?? true) { - this.clearMongooseModels(this.mongooseService); + while (remaining.length > 0) { + const wave = remaining.filter((registration) => registration.dependsOn.every((dependency) => started.has(dependency))); + if (wave.length === 0) { + throw new Error(`ApiInfrastructure: circular or unresolved dependencies among ${remaining.map((registration) => registration.name).join(', ')}`); } - } - return this.mongooseService; + await Promise.all(wave.map((registration) => this.startServer(registration))); + + for (const registration of wave) { + started.add(registration.name); + } + remaining = remaining.filter((registration) => !started.has(registration.name)); + } } - private getMongooseService(): TMongooseService { - if (!this.mongooseService) { - throw new Error('ApiInfrastructure Mongoose service is not configured or has not started'); + private async startServer(registration: ServerRegistration): Promise { + let server = this.created.get(registration.name); + if (!server) { + server = registration.factory(this.context); + this.created.set(registration.name, server); } - return this.mongooseService; - } + if (!server.isRunning()) { + await server.start(); + } - private clearMongooseModels(mongooseService: ManagedMongooseService): void { - for (const modelName of Object.keys(mongooseService.service.connection.models)) { - try { - mongooseService.service.connection.deleteModel(modelName); - } catch { - /* already deleted */ - } + if (!this.startOrder.includes(registration.name)) { + this.startOrder.push(registration.name); } } } diff --git a/packages/cellix/serenity-framework/src/infrastructure/index.ts b/packages/cellix/serenity-framework/src/infrastructure/index.ts index 898ed9e0e..ffd562641 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/index.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/index.ts @@ -1,13 +1,4 @@ -export type { - ApiInfrastructureMongooseOptions, - ApiInfrastructureOptions, - ApiInfrastructureState, - ApiMongoServerFactory, - GraphqlServerFactory, - GraphqlServerFactoryContext, - ManagedMongooseConnection, - ManagedMongooseService, -} from './api-infrastructure.ts'; +export type { ApiInfrastructureOptions, ApiInfrastructureState, ApiServerContext, ApiServerFactory, ApiServerOptions } from './api-infrastructure.ts'; export { ApiInfrastructure } from './api-infrastructure.ts'; export type { E2EInfrastructureOptions, E2EInfrastructureState, E2EServerContext, E2EServerFactory, E2EServerOptions, E2EUiPortalOptions } from './e2e-infrastructure.ts'; export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts index d0361b2ae..9bcb3d5ee 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts @@ -47,14 +47,6 @@ function mongoServer(url = 'mongodb://test'): FakeServer & MongoMemoryTestServer return new FakeServer(url) as FakeServer & MongoMemoryTestServer; } -function mongoOptions() { - return { - dbName: 'test', - port: 27_017, - replSetName: 'rs0', - }; -} - function browserStubs() { const page = { close: vi.fn(), @@ -73,43 +65,49 @@ function browserStubs() { } describe('ApiInfrastructure', () => { - it('starts MongoDB before the GraphQL server and exposes the GraphQL URL', async () => { + it('starts servers in dependency order and exposes them by name, wiring the factory context', async () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); - const infrastructure = ApiInfrastructure.using({ - createGraphqlServer: () => graphQL, - createMongoServer: () => mongo, - mongoServer: mongoOptions(), - }); + let graphqlSawMongoUrl: string | undefined; + const infrastructure = ApiInfrastructure.create() + .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) + .addServer( + 'graphql', + (ctx) => { + graphqlSawMongoUrl = ctx.server('mongo').getUrl(); + return graphQL; + }, + { dependsOn: ['mongo'] }, + ); await infrastructure.ensureStarted(); - expect(infrastructure.getState().graphqlUrl).toBe('http://127.0.0.1:4000/graphql'); + expect(graphqlSawMongoUrl).toBe('mongodb://test'); + expect(Object.keys(infrastructure.getState().servers)).toEqual(['mongo', 'graphql']); + expect(graphQL.getUrl()).toBe('http://127.0.0.1:4000/graphql'); expect(graphQL.startCalls).toBe(1); }); - it('runs the GraphQL server without a database when no mongoServer is configured', async () => { + it('runs a single server without a database when none is registered', async () => { const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); - const infrastructure = ApiInfrastructure.using({ createGraphqlServer: () => graphQL }); + const infrastructure = ApiInfrastructure.create().addServer('graphql', () => graphQL); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); - expect(infrastructure.getState().graphqlUrl).toBe('http://127.0.0.1:4000/graphql'); - expect(infrastructure.getState().mongoServer).toBeUndefined(); + expect(Object.keys(infrastructure.getState().servers)).toEqual(['graphql']); + expect(graphQL.getUrl()).toBe('http://127.0.0.1:4000/graphql'); expect(graphQL.startCalls).toBe(1); }); - it('resets MongoDB without restarting the GraphQL server between scenarios', async () => { + it('resets a server without restarting others between scenarios', async () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); - const infrastructure = ApiInfrastructure.using({ - createGraphqlServer: () => graphQL, - createMongoServer: () => mongo, - mongoServer: mongoOptions(), - }); + const infrastructure = ApiInfrastructure.create() + .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) + .addServer('graphql', () => graphQL, { dependsOn: ['mongo'] }); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); @@ -118,6 +116,18 @@ describe('ApiInfrastructure', () => { expect(mongo.resetCalls).toBe(1); expect(graphQL.startCalls).toBe(1); }); + + it('rejects duplicate server names', () => { + const infrastructure = ApiInfrastructure.create().addServer('graphql', () => new FakeServer('http://127.0.0.1:4000/graphql')); + + expect(() => infrastructure.addServer('graphql', () => new FakeServer('http://127.0.0.1:4001/graphql'))).toThrow(/already registered/); + }); + + it('rejects an unknown dependency', async () => { + const infrastructure = ApiInfrastructure.create().addServer('graphql', () => new FakeServer('http://127.0.0.1:4000/graphql'), { dependsOn: ['mongo'] }); + + await expect(infrastructure.ensureStarted()).rejects.toThrow(/unknown server 'mongo'/); + }); }); describe('E2EInfrastructure', () => { diff --git a/packages/ocom-verification/acceptance-api/src/infrastructure.ts b/packages/ocom-verification/acceptance-api/src/infrastructure.ts index 24b2c01b3..d16350f87 100644 --- a/packages/ocom-verification/acceptance-api/src/infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/infrastructure.ts @@ -1,31 +1,35 @@ -import { ApiInfrastructure, type ApiInfrastructureState } from '@cellix/serenity-framework/infrastructure/api'; -import type { ServiceMongoose } from '@ocom/service-mongoose'; +import { ApiInfrastructure } from '@cellix/serenity-framework/infrastructure/api'; +import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; +import { ServiceMongoose } from '@ocom/service-mongoose'; import { getMongoPort } from '@ocom-verification/verification-shared/environment'; import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; -import { createApiGraphQLServer, createApiMongooseService, resetApiGraphQLServerFactories } from './test-server-factories.ts'; +import { ApiGraphQLTestServer, MongooseTestServer } from './test-server-factories.ts'; const apiDbName = 'owner-community'; -const infrastructure = ApiInfrastructure.using({ - createGraphqlServer: ({ getMongooseService }) => createApiGraphQLServer(getMongooseService), - mongoServer: { - dbName: apiDbName, - port: getMongoPort(), - replSetName: 'globaldb', - seedData: seedDatabase, - }, - mongoose: { - createService: (connectionString) => createApiMongooseService(connectionString, apiDbName), - }, -}); +const infrastructure = ApiInfrastructure.create() + .addServer('mongo', () => new MongoMemoryTestServer({ dbName: apiDbName, port: getMongoPort(), replSetName: 'globaldb', seedData: seedDatabase }), { + resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), + }) + .addServer('mongoose', (ctx) => new MongooseTestServer(() => new ServiceMongoose(ctx.server('mongo').getConnectionString(), { autoCreate: true, autoIndex: true, dbName: apiDbName })), { + dependsOn: ['mongo'], + }) + .addServer('graphql', (ctx) => new ApiGraphQLTestServer(() => ctx.server('mongoose').getService()), { dependsOn: ['mongoose'] }); -export function getState(): ApiInfrastructureState { - return infrastructure.getState(); +interface ApiAcceptanceState { + graphqlUrl: string | undefined; +} + +export function getState(): ApiAcceptanceState { + // biome-ignore lint:useLiteralKeys - servers is an index-signature record; bracket access required by noPropertyAccessFromIndexSignature + const graphqlServer = infrastructure.getState().servers['graphql']; + return { + graphqlUrl: graphqlServer?.isRunning() ? graphqlServer.getUrl() : undefined, + }; } export async function stopAll(): Promise { await infrastructure.stopAll(); - resetApiGraphQLServerFactories(); } export async function ensureApiServers(): Promise { diff --git a/packages/ocom-verification/acceptance-api/src/shared/application-services/mock-application-services.ts b/packages/ocom-verification/acceptance-api/src/mock-application-services.ts similarity index 100% rename from packages/ocom-verification/acceptance-api/src/shared/application-services/mock-application-services.ts rename to packages/ocom-verification/acceptance-api/src/mock-application-services.ts diff --git a/packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts b/packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts deleted file mode 100644 index 8f18e4ea1..000000000 --- a/packages/ocom-verification/acceptance-api/src/shared/application-services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createMockApplicationServicesFactory } from './mock-application-services.ts'; diff --git a/packages/ocom-verification/acceptance-api/src/test-server-factories.ts b/packages/ocom-verification/acceptance-api/src/test-server-factories.ts index 1e23061c8..cb7526acc 100644 --- a/packages/ocom-verification/acceptance-api/src/test-server-factories.ts +++ b/packages/ocom-verification/acceptance-api/src/test-server-factories.ts @@ -1,36 +1,103 @@ -import { ApolloGraphQLTestServer } from '@cellix/serenity-framework/servers'; +import { ApolloGraphQLTestServer, type TestServer } from '@cellix/serenity-framework/servers'; import type { ApplicationServices } from '@ocom/application-services'; import { combinedSchema } from '@ocom/graphql'; -import { ServiceMongoose } from '@ocom/service-mongoose'; +import type { ServiceMongoose } from '@ocom/service-mongoose'; import depthLimit from 'graphql-depth-limit'; import { applyMiddleware } from 'graphql-middleware'; -import { createMockApplicationServicesFactory } from './shared/application-services/index.ts'; +import { createMockApplicationServicesFactory } from './mock-application-services.ts'; -let mockApplicationServicesFactory: ReturnType | undefined; +/** + * {@link TestServer} adapter that owns the Mongoose connection lifecycle for the + * acceptance suite. The framework is database-ignorant, so connecting, clearing + * registered models, and disconnecting all live here. Exposes the started + * {@link ServiceMongoose} to dependent servers (e.g. the GraphQL server) via + * {@link getService}. + */ +export class MongooseTestServer implements TestServer { + private serviceInternal: ServiceMongoose | undefined; -export function createApiMongooseService(connectionString: string, dbName: string): ServiceMongoose { - return new ServiceMongoose(connectionString, { - autoCreate: true, - autoIndex: true, - dbName, - }); -} + constructor(private readonly createService: () => ServiceMongoose) {} -export function createApiGraphQLServer(getMongooseService: () => ServiceMongoose): ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }> { - return new ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>({ - schema: applyMiddleware(combinedSchema), - validationRules: [depthLimit(10)], - context: async ({ req }) => { - mockApplicationServicesFactory ??= createMockApplicationServicesFactory(getMongooseService()); - const applicationServices = await mockApplicationServicesFactory.forRequest(req.headers.authorization ?? undefined); - if (!applicationServices) { - throw new Error('ApplicationServicesFactory required for test server'); + async start(): Promise { + const service = this.createService(); + await service.startUp(); + // Clear any models registered on a previous connection so schemas re-register cleanly. + const { connection } = service.service; + for (const modelName of Object.keys(connection.models)) { + try { + connection.deleteModel(modelName); + } catch { + /* already deleted */ } - return { applicationServices }; - }, - }); + } + this.serviceInternal = service; + } + + async stop(): Promise { + if (this.serviceInternal) { + await this.serviceInternal.shutDown(); + this.serviceInternal = undefined; + } + } + + isRunning(): boolean { + return this.serviceInternal !== undefined; + } + + /** Not a network server; no URL is exposed. */ + getUrl(): string { + return ''; + } + + /** The started Mongoose service. Throws if accessed before {@link start}. */ + getService(): ServiceMongoose { + if (!this.serviceInternal) { + throw new Error('MongooseTestServer not started'); + } + return this.serviceInternal; + } } -export function resetApiGraphQLServerFactories(): void { - mockApplicationServicesFactory = undefined; +/** + * {@link TestServer} that owns the Apollo GraphQL server lifecycle for the + * acceptance suite. It wires the app's schema and permissions middleware and + * builds a request-scoped mock application-services factory lazily on the first + * request, caching it for the lifetime of the running server. The cache is + * instance-scoped, so it is discarded when the server stops. + */ +export class ApiGraphQLTestServer implements TestServer { + private readonly server: ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>; + private applicationServicesFactory: ReturnType | undefined; + + constructor(getMongooseService: () => ServiceMongoose) { + this.server = new ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>({ + schema: applyMiddleware(combinedSchema), + validationRules: [depthLimit(10)], + context: async ({ req }) => { + this.applicationServicesFactory ??= createMockApplicationServicesFactory(getMongooseService()); + const applicationServices = await this.applicationServicesFactory.forRequest(req.headers.authorization ?? undefined); + if (!applicationServices) { + throw new Error('ApplicationServicesFactory required for test server'); + } + return { applicationServices }; + }, + }); + } + + start(): Promise { + return this.server.start(); + } + + async stop(): Promise { + await this.server.stop(); + this.applicationServicesFactory = undefined; + } + + isRunning(): boolean { + return this.server.isRunning(); + } + + getUrl(): string { + return this.server.getUrl(); + } } From 0c1ae41f5e70eb8941c66afd8ddd25ae62c58246 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 4 Jun 2026 10:25:21 -0400 Subject: [PATCH 18/40] small adjustments based on codex review --- packages/cellix/serenity-framework/README.md | 29 ++++---- .../src/infrastructure/api-infrastructure.ts | 14 +++- .../src/infrastructure/e2e-infrastructure.ts | 14 +++- .../src/infrastructure/infrastructure.test.ts | 68 ++++++++++++++++++- .../src/servers/process-test-server.test.ts | 53 +++++++++++++++ .../src/servers/process-test-server.ts | 16 ++++- 6 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 packages/cellix/serenity-framework/src/servers/process-test-server.test.ts diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index b8ddaa3da..bf628eeeb 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -77,23 +77,26 @@ A server factory receives a context so a dependent server (such as the API) can ## API acceptance infrastructure -API-only acceptance suites use the smaller infrastructure manager. It requires MongoDB options and an API server factory, and owns startup, URL state, reset, and shutdown. If the API needs a Mongoose service, pass a factory that returns the consumer's Mongoose-compatible service; the framework starts it, clears registered models, and stops it. +API-only acceptance suites use the smaller infrastructure manager. It composes any consumer-owned `TestServer` implementations without launching a browser, and owns dependency-ordered startup, scenario reset, and shutdown. ```ts import { ApiInfrastructure } from '@cellix/serenity-framework/infrastructure/api'; -import { ApolloGraphQLTestServer } from '@cellix/serenity-framework/servers'; +import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; -export const infrastructure = ApiInfrastructure.using({ - mongoServer: { dbName, port, replSetName, seedData }, - mongoose: { - createService: (connectionString) => createMongooseService(connectionString), - }, - createApiServer: ({ getMongooseService }) => - new ApolloGraphQLTestServer({ - schema, - context: () => createContext(getMongooseService()), - }), -}); +export const infrastructure = ApiInfrastructure + .create() + .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { + resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), + }) + .addServer( + 'graphql', + (ctx) => createGraphqlServer(ctx.server('mongo').getConnectionString()), + { dependsOn: ['mongo'] }, + ); + +// Suites without a database can register only their API server. +const apiOnly = ApiInfrastructure.create() + .addServer('graphql', () => createGraphqlServer()); ``` ## Managed worlds diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts index 397f4b512..4c47b4817 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -150,9 +150,13 @@ export class ApiInfrastructure { } } - /** Stop every server (in reverse start order) and the suite environment, swallowing shutdown errors. */ + /** Stop every created server, including partial starts, then clean up the suite environment. */ async stopAll(): Promise { - for (const name of [...this.startOrder].reverse()) { + const tracked = new Set(this.startOrder); + const createdButUntracked = [...this.created.keys()].filter((name) => !tracked.has(name)).reverse(); + const stopOrder = [...createdButUntracked, ...[...this.startOrder].reverse()]; + + for (const name of stopOrder) { await this.created .get(name) ?.stop() @@ -222,7 +226,11 @@ export class ApiInfrastructure { throw new Error(`ApiInfrastructure: circular or unresolved dependencies among ${remaining.map((registration) => registration.name).join(', ')}`); } - await Promise.all(wave.map((registration) => this.startServer(registration))); + const results = await Promise.allSettled(wave.map((registration) => this.startServer(registration))); + const failure = results.find((result) => result.status === 'rejected'); + if (failure) { + throw failure.reason; + } for (const registration of wave) { started.add(registration.name); diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts index eae26bdeb..d9d9e9514 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts @@ -210,11 +210,15 @@ export class E2EInfrastructure { } } - /** Stop browser resources, every server (in reverse start order), and the suite environment. */ + /** Stop browser resources, every created server including partial starts, and the suite environment. */ async stopAll(): Promise { await this.closeBrowser(); - for (const name of [...this.startOrder].reverse()) { + const tracked = new Set(this.startOrder); + const createdButUntracked = [...this.created.keys()].filter((name) => !tracked.has(name)).reverse(); + const stopOrder = [...createdButUntracked, ...[...this.startOrder].reverse()]; + + for (const name of stopOrder) { await this.created .get(name) ?.stop() @@ -315,7 +319,11 @@ export class E2EInfrastructure { throw new Error(`E2EInfrastructure: circular or unresolved dependencies among ${remaining.map((registration) => registration.name).join(', ')}`); } - await Promise.all(wave.map((registration) => this.startServer(registration))); + const results = await Promise.allSettled(wave.map((registration) => this.startServer(registration))); + const failure = results.find((result) => result.status === 'rejected'); + if (failure) { + throw failure.reason; + } for (const registration of wave) { started.add(registration.name); diff --git a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts index 9bcb3d5ee..7b32b5bcf 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts @@ -13,7 +13,7 @@ class FakeServer implements TestServer { startCalls = 0; stopCalls = 0; resetCalls = 0; - private running = false; + protected running = false; constructor(private readonly url: string) {} @@ -43,6 +43,28 @@ class FakeServer implements TestServer { } } +class DeferredServer extends FakeServer { + constructor( + url: string, + private readonly startGate: Promise, + ) { + super(url); + } + + override async start(): Promise { + this.startCalls += 1; + await this.startGate; + this.running = true; + } +} + +class FailingServer extends FakeServer { + override start(): Promise { + this.startCalls += 1; + return Promise.reject(new Error('startup failed')); + } +} + function mongoServer(url = 'mongodb://test'): FakeServer & MongoMemoryTestServer { return new FakeServer(url) as FakeServer & MongoMemoryTestServer; } @@ -128,6 +150,28 @@ describe('ApiInfrastructure', () => { await expect(infrastructure.ensureStarted()).rejects.toThrow(/unknown server 'mongo'/); }); + + it('waits for a failed startup wave to settle and stops every created server', async () => { + let releaseStart: () => void = () => undefined; + const startGate = new Promise((resolve) => { + releaseStart = resolve; + }); + const slow = new DeferredServer('http://slow.test', startGate); + const failing = new FailingServer('http://failing.test'); + const infrastructure = ApiInfrastructure.create() + .addServer('slow', () => slow) + .addServer('failing', () => failing); + + const starting = expect(infrastructure.ensureStarted()).rejects.toThrow('startup failed'); + await vi.waitFor(() => expect(failing.startCalls).toBe(1)); + releaseStart(); + + await starting; + expect(slow.stopCalls).toBe(1); + expect(failing.stopCalls).toBe(1); + expect(slow.isRunning()).toBe(false); + expect(infrastructure.getState().servers).toEqual({}); + }); }); describe('E2EInfrastructure', () => { @@ -185,4 +229,26 @@ describe('E2EInfrastructure', () => { expect(infrastructure.getState().browseTheWeb).toBeDefined(); }); + + it('waits for a failed startup wave to settle and stops every created server', async () => { + let releaseStart: () => void = () => undefined; + const startGate = new Promise((resolve) => { + releaseStart = resolve; + }); + const slow = new DeferredServer('http://slow.test', startGate); + const failing = new FailingServer('http://failing.test'); + const infrastructure = E2EInfrastructure.create() + .addServer('slow', () => slow) + .addServer('failing', () => failing); + + const starting = expect(infrastructure.ensureStarted()).rejects.toThrow('startup failed'); + await vi.waitFor(() => expect(failing.startCalls).toBe(1)); + releaseStart(); + + await starting; + expect(slow.stopCalls).toBe(1); + expect(failing.stopCalls).toBe(1); + expect(slow.isRunning()).toBe(false); + expect(infrastructure.getState().servers).toEqual({}); + }); }); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts new file mode 100644 index 000000000..51f05e970 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { ProcessTestServer } from './index.ts'; + +async function waitUntil(predicate: () => boolean, timeoutMs = 2_000): Promise { + const deadline = Date.now() + timeoutMs; + while (!predicate()) { + if (Date.now() >= deadline) { + throw new Error(`Condition not met within ${timeoutMs}ms`); + } + await new Promise((resolve) => setTimeout(resolve, 10)); + } +} + +describe('ProcessTestServer', () => { + it('starts a process and trusts the ready marker when probing is disabled', async () => { + const server = new ProcessTestServer({ + serverName: 'marker-only server', + executable: process.execPath, + spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], + cwd: process.cwd(), + readyMarker: 'READY', + getUrl: () => 'http://unused.test', + probe: false, + shutdownTimeoutMs: 500, + }); + + try { + await server.start(); + + expect(server.isRunning()).toBe(true); + } finally { + await server.stop(); + } + }); + + it('stops reporting a process as running after it exits', async () => { + const server = new ProcessTestServer({ + serverName: 'short-lived server', + executable: process.execPath, + spawnArgs: ['-e', "console.log('READY'); setTimeout(() => process.exit(0), 20)"], + cwd: process.cwd(), + readyMarker: 'READY', + getUrl: () => 'http://unused.test', + probe: false, + shutdownTimeoutMs: 500, + }); + + await server.start(); + await waitUntil(() => !server.isRunning()); + + expect(server.isRunning()).toBe(false); + }); +}); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.ts index 98954d96b..6491e0732 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.ts @@ -101,7 +101,12 @@ export class ProcessTestServer implements TestServer { }); this.startedByUs = true; - await this.waitForReady(); + try { + await this.waitForReady(); + } catch (error) { + await this.stop().catch(() => undefined); + throw error; + } } /** @@ -149,6 +154,9 @@ export class ProcessTestServer implements TestServer { if (this.options.isAlreadyRunning) { return await this.options.isAlreadyRunning(); } + if (this.options.probe === false) { + return false; + } return await this.isProbeReadyWithin(this.value(this.options.healthProbeTimeoutMs) ?? 3_000); } @@ -205,12 +213,14 @@ export class ProcessTestServer implements TestServer { }); childProcess.on('exit', (code, signal) => { + if (this.process === childProcess) { + this.process = null; + this.startedByUs = false; + } if (ready) { return; } clearTimeout(timeout); - this.process = null; - this.startedByUs = false; if (this.options.isReusableExit?.(stderrOutput)) { resolve(); From efa91348b5ae0f24d7894d6dbf38c763ef5d5e1b Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 4 Jun 2026 11:15:08 -0400 Subject: [PATCH 19/40] initial commit for verify --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index a997ba1ed..9de01cabd 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,7 @@ Domain-driven architecture for Azure Functions with GraphQL/REST, MongoDB (Mongo + [Getting Started](https://developers.cellixjs.org/docs/intro): Our Docusaurus website will help you get started with running and contributing to CellixJS From dfdfb1520831cf56410ac3011452fc244e8f945e Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 4 Jun 2026 11:44:41 -0400 Subject: [PATCH 20/40] initial script extraction - wip --- apps/api/package.json | 9 +- apps/api/scripts/sync-local-settings.mjs | 63 ----- apps/api/start-azurite.mjs | 47 ---- apps/api/start-dev.mjs | 47 ---- apps/api/turbo.json | 2 +- apps/docs/package.json | 5 +- apps/docs/start-dev.mjs | 10 - apps/docs/turbo.json | 4 +- apps/server-mongodb-memory-mock/package.json | 6 +- .../start-mongo.mjs | 12 - apps/server-oauth2-mock/package.json | 5 +- apps/server-oauth2-mock/start-dev.mjs | 20 -- apps/server-oauth2-mock/turbo.json | 2 +- apps/ui-community/package.json | 5 +- apps/ui-community/start-dev.mjs | 24 -- apps/ui-community/turbo.json | 4 +- apps/ui-staff/package.json | 5 +- apps/ui-staff/start-dev.mjs | 22 -- biome.json | 2 +- knip.json | 36 ++- packages/cellix/local-dev/.gitignore | 2 + packages/cellix/local-dev/README.md | 89 +++++++ .../cellix/local-dev/bin/cellix-local-dev.mjs | 22 ++ packages/cellix/local-dev/manifest.md | 60 +++++ packages/cellix/local-dev/package.json | 42 +++ .../local-dev/src/api-local-settings.ts | 85 ++++++ packages/cellix/local-dev/src/bin.ts | 7 + packages/cellix/local-dev/src/cli.ts | 121 +++++++++ packages/cellix/local-dev/src/dev-process.ts | 24 ++ packages/cellix/local-dev/src/hostnames.ts | 91 +++++++ packages/cellix/local-dev/src/index.test.ts | 137 ++++++++++ packages/cellix/local-dev/src/index.ts | 31 +++ packages/cellix/local-dev/src/runners.ts | 243 ++++++++++++++++++ packages/cellix/local-dev/src/vite.ts | 31 +++ packages/cellix/local-dev/src/workspace.ts | 26 ++ .../cellix/local-dev/src/worktree-ports.ts | 107 ++++++++ packages/cellix/local-dev/tsconfig.json | 9 + .../cellix/local-dev/tsconfig.vitest.json | 9 + packages/cellix/local-dev/vitest.config.ts | 18 ++ .../src/servers/process-test-server.test.ts | 31 ++- .../src/servers/process-test-server.ts | 8 +- .../e2e-tests/src/test-server-factories.ts | 11 +- .../ocom-verification/e2e-tests/turbo.json | 6 +- pnpm-lock.yaml | 63 ++++- scripts/local-dev/dev-process-exit.mjs | 25 -- scripts/local-dev/portless-hostnames.mjs | 97 ------- scripts/local-dev/vite-dev-args.mjs | 26 -- scripts/local-dev/worktree-ports.mjs | 130 ---------- sonar-project.properties | 2 +- 49 files changed, 1290 insertions(+), 593 deletions(-) delete mode 100644 apps/api/scripts/sync-local-settings.mjs delete mode 100644 apps/api/start-azurite.mjs delete mode 100644 apps/api/start-dev.mjs delete mode 100644 apps/docs/start-dev.mjs delete mode 100644 apps/server-mongodb-memory-mock/start-mongo.mjs delete mode 100644 apps/server-oauth2-mock/start-dev.mjs delete mode 100644 apps/ui-community/start-dev.mjs delete mode 100644 apps/ui-staff/start-dev.mjs create mode 100644 packages/cellix/local-dev/.gitignore create mode 100644 packages/cellix/local-dev/README.md create mode 100755 packages/cellix/local-dev/bin/cellix-local-dev.mjs create mode 100644 packages/cellix/local-dev/manifest.md create mode 100644 packages/cellix/local-dev/package.json create mode 100644 packages/cellix/local-dev/src/api-local-settings.ts create mode 100644 packages/cellix/local-dev/src/bin.ts create mode 100644 packages/cellix/local-dev/src/cli.ts create mode 100644 packages/cellix/local-dev/src/dev-process.ts create mode 100644 packages/cellix/local-dev/src/hostnames.ts create mode 100644 packages/cellix/local-dev/src/index.test.ts create mode 100644 packages/cellix/local-dev/src/index.ts create mode 100644 packages/cellix/local-dev/src/runners.ts create mode 100644 packages/cellix/local-dev/src/vite.ts create mode 100644 packages/cellix/local-dev/src/workspace.ts create mode 100644 packages/cellix/local-dev/src/worktree-ports.ts create mode 100644 packages/cellix/local-dev/tsconfig.json create mode 100644 packages/cellix/local-dev/tsconfig.vitest.json create mode 100644 packages/cellix/local-dev/vitest.config.ts delete mode 100644 scripts/local-dev/dev-process-exit.mjs delete mode 100644 scripts/local-dev/portless-hostnames.mjs delete mode 100644 scripts/local-dev/vite-dev-args.mjs delete mode 100644 scripts/local-dev/worktree-ports.mjs diff --git a/apps/api/package.json b/apps/api/package.json index f4d026525..330501df0 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,9 +9,9 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.mjs", + "dev": "pnpm exec portless data-access.ownercommunity.localhost --force cellix-local-dev azure-functions", "predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev azure-functions", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", @@ -23,8 +23,8 @@ "clean": "rimraf dist deploy", "prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings", "start": "func start --typescript --script-root deploy/", - "sync-local-settings": "node scripts/sync-local-settings.mjs", - "azurite": "node start-azurite.mjs" + "sync-local-settings": "cellix-local-dev sync-api-local-settings", + "azurite": "cellix-local-dev azurite" }, "dependencies": { "@azure/functions": "catalog:", @@ -48,6 +48,7 @@ "@cellix/config-rolldown": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", + "@cellix/local-dev": "workspace:*", "@vitest/coverage-istanbul": "catalog:", "azurite": "^3.35.0", "rimraf": "catalog:", diff --git a/apps/api/scripts/sync-local-settings.mjs b/apps/api/scripts/sync-local-settings.mjs deleted file mode 100644 index d9b3a7acc..000000000 --- a/apps/api/scripts/sync-local-settings.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { buildPortlessUrl, getHostnames } from '../../../scripts/local-dev/portless-hostnames.mjs'; -import { getAzuriteConnectionString } from '../../../scripts/local-dev/worktree-ports.mjs'; - -const apiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const mode = process.argv[2] ?? (isE2E() ? 'e2e' : undefined); -const localSettingsPath = path.join(apiDir, 'local.settings.json'); -const e2eLocalSettingsPath = path.join(apiDir, 'local-settings.e2e.json'); -const targetPath = path.join(apiDir, 'deploy', 'local.settings.json'); - -mkdirSync(path.dirname(targetPath), { recursive: true }); - -if (!mode) { - if (existsSync(localSettingsPath)) { - copyFileSync(localSettingsPath, targetPath); - } - process.exit(0); -} - -if (mode !== 'e2e') { - throw new Error('[sync-local-settings] Invalid mode: expected one of e2e'); -} - -if (!existsSync(e2eLocalSettingsPath)) { - throw new Error(`[sync-local-settings] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`); -} - -const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf-8')); -applyE2EOverrides(settings); -writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`); - -function applyE2EOverrides(settings) { - const values = { ...(settings.Values ?? {}) }; - - // Worktree-scoped overrides: when WORKTREE_NAME is set the proxy hostnames - // and Azurite ports are scoped to that worktree, so we rewrite the URLs and - // connection strings here. Without WORKTREE_NAME the committed JSON values - // already match the default hostnames, so we leave them alone. - if (process.env.WORKTREE_NAME) { - const hostnames = getHostnames(); - values.ACCOUNT_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/community'); - values.ACCOUNT_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); - values.STAFF_PORTAL_OIDC_ISSUER = buildPortlessUrl(hostnames.mockAuth, '/staff'); - values.STAFF_PORTAL_OIDC_ENDPOINT = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); - const azurite = getAzuriteConnectionString(values); - values.AZURE_STORAGE_CONNECTION_STRING = azurite; - values.AzureWebJobsStorage = azurite; - } - - // Runtime-only injection: the e2e harness spawns MongoMemoryServer on a - // random port and passes the connection string through process.env. - if (process.env.COSMOSDB_CONNECTION_STRING) { - values.COSMOSDB_CONNECTION_STRING = process.env.COSMOSDB_CONNECTION_STRING; - } - - settings.Values = values; -} - -function isE2E() { - return ['1', 'true', 'yes'].includes((process.env.E2E ?? '').toLowerCase()); -} diff --git a/apps/api/start-azurite.mjs b/apps/api/start-azurite.mjs deleted file mode 100644 index 8cb110b31..000000000 --- a/apps/api/start-azurite.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { spawn } from 'node:child_process'; -import { isGracefulInterruptExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { getAzuritePorts } from '../../scripts/local-dev/worktree-ports.mjs'; - -const ports = getAzuritePorts(); -const worktreeName = process.env.WORKTREE_NAME ?? ''; -const storageSuffix = worktreeName ? `-${worktreeName}` : ''; - -const blobDir = `../../__blobstorage__${storageSuffix}`; -const queueDir = `../../__queuestorage__${storageSuffix}`; -const tableDir = `../../__tablestorage__${storageSuffix}`; - -const procSpecs = [ - ['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', blobDir]], - ['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', queueDir]], - ['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', tableDir]], -]; -const procs = procSpecs.map(([command, args]) => { - const proc = spawn(command, args, { stdio: 'inherit' }); - proc.on('error', (error) => { - console.error(`[azurite] failed to start ${command}: ${error.message}`); - for (const p of procs) p.kill(); - process.exit(1); - }); - return proc; -}); - -console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`); - -let exited = 0; -for (const proc of procs) { - proc.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - if (++exited === procs.length) process.exit(0); - return; - } - console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); - for (const p of procs) p.kill(); - process.exit(code ?? 1); - }); -} -process.on('SIGINT', () => { - for (const p of procs) p.kill('SIGINT'); -}); -process.on('SIGTERM', () => { - for (const p of procs) p.kill('SIGTERM'); -}); diff --git a/apps/api/start-dev.mjs b/apps/api/start-dev.mjs deleted file mode 100644 index 17cb96ad8..000000000 --- a/apps/api/start-dev.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { spawn } from 'node:child_process'; -import os from 'node:os'; -import path from 'node:path'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; -import { getAzuriteConnectionString, getMongoConnectionString } from '../../scripts/local-dev/worktree-ports.mjs'; - -const envPort = process.env.PORT; - -if (!envPort) { - console.error('PORT environment variable is not set. Start this command through portless.'); - process.exit(1); -} - -const portlessCaPath = process.env.PORTLESS_CA_PATH ?? path.join(os.homedir(), '.portless', 'ca.pem'); - -const childEnv = { - ...process.env, - NODE_EXTRA_CA_CERTS: portlessCaPath, - NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), -}; - -// Only inject worktree-scoped overrides when running in worktree mode. -// When WORKTREE_NAME is absent, local.settings.json remains the source of truth. -// Use `??=` so callers can override any individual value via process.env. -if (process.env.WORKTREE_NAME) { - const hostnames = getHostnames(); - childEnv.ACCOUNT_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/community'); - childEnv.ACCOUNT_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); - childEnv.STAFF_PORTAL_OIDC_ISSUER ??= buildPortlessUrl(hostnames.mockAuth, '/staff'); - childEnv.STAFF_PORTAL_OIDC_ENDPOINT ??= buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); - childEnv.COSMOSDB_CONNECTION_STRING ??= getMongoConnectionString(); - childEnv.AZURE_STORAGE_CONNECTION_STRING ??= getAzuriteConnectionString(); - childEnv.AzureWebJobsStorage ??= getAzuriteConnectionString(); - // Disable the Node.js inspector — port 5858 is already used by the primary worktree. - childEnv.languageWorkers__node__arguments ??= ''; -} - -// `--cors '*'` matches Host.CORS in local.settings.json but does not depend on -// that file existing — local.settings.json is gitignored, so CI has no CORS -// allowance otherwise and the UI's cross-origin GraphQL requests are blocked. -const child = spawn('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { - stdio: 'inherit', - env: childEnv, -}); - -forwardChildExit(child); diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 75ff6794b..378439790 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -4,7 +4,7 @@ "build": { "cache": true, "dependsOn": ["^build", "//#gen"], - "inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json", "$TURBO_ROOT$/scripts/local-dev/**"], + "inputs": ["$TURBO_EXTENDS$", "rolldown.config.ts", "host.json"], "outputs": ["$TURBO_EXTENDS$", "deploy/**"] }, "dev": { diff --git a/apps/docs/package.json b/apps/docs/package.json index 0d63617a3..a8a526048 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.mjs", - "dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev": "pnpm exec portless docs.ownercommunity.localhost --force cellix-local-dev docusaurus", + "dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev docusaurus", "start": "docusaurus start --port 3001", "build": "docusaurus build", "swizzle": "docusaurus swizzle", @@ -32,6 +32,7 @@ "devDependencies": { "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", + "@cellix/local-dev": "workspace:*", "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", diff --git a/apps/docs/start-dev.mjs b/apps/docs/start-dev.mjs deleted file mode 100644 index 8180032bb..000000000 --- a/apps/docs/start-dev.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { spawn } from 'node:child_process'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; - -const port = process.env.PORT ?? '3001'; - -const child = spawn('docusaurus', ['start', '--port', port, '--host', '127.0.0.1', '--no-open'], { - stdio: 'inherit', -}); - -forwardChildExit(child); diff --git a/apps/docs/turbo.json b/apps/docs/turbo.json index 304f1bb57..62eb2f5e0 100644 --- a/apps/docs/turbo.json +++ b/apps/docs/turbo.json @@ -5,13 +5,13 @@ "dependsOn": [], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] + "inputs": [".env", "package.json", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, "dev:worktree": { "dependsOn": [], "persistent": true, "interruptible": false, - "inputs": [".env", "package.json", "start-dev.mjs", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] + "inputs": [".env", "package.json", "docusaurus.config.ts", "sidebars.ts", "tsconfig.json"] }, "test": { "inputs": ["$TURBO_EXTENDS$", "!docs/**", "!blog/**", "!static/**"] diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index ed4a774d3..96109e079 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -11,17 +11,17 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "tsx src/index.ts", - "dev:worktree": "node start-mongo.mjs" + "dev": "cellix-local-dev tsx --profile mongo-memory-mock", + "dev:worktree": "cellix-local-dev tsx --profile mongo-memory-mock" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", "dotenv": "^16.4.5" }, "devDependencies": { + "@cellix/local-dev": "workspace:*", "@cellix/config-typescript": "workspace:*", "rimraf": "catalog:", - "tsx": "catalog:", "typescript": "catalog:" } } diff --git a/apps/server-mongodb-memory-mock/start-mongo.mjs b/apps/server-mongodb-memory-mock/start-mongo.mjs deleted file mode 100644 index cead8110f..000000000 --- a/apps/server-mongodb-memory-mock/start-mongo.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { spawn } from 'node:child_process'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { getMongoPort } from '../../scripts/local-dev/worktree-ports.mjs'; - -const MONGO_PORT = getMongoPort(); - -const child = spawn('tsx', ['src/index.ts'], { - stdio: 'inherit', - env: { ...process.env, PORT: String(MONGO_PORT) }, -}); - -forwardChildExit(child); diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index 7201d84b9..6c0c8ae07 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -11,8 +11,8 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force tsx src/index.ts", - "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force cellix-local-dev tsx --profile oauth2-mock", + "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev tsx --profile oauth2-mock", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest" @@ -22,6 +22,7 @@ "dotenv": "^16.4.5" }, "devDependencies": { + "@cellix/local-dev": "workspace:*", "@cellix/config-vitest": "workspace:*", "@cellix/config-typescript": "workspace:*", "@vitest/coverage-istanbul": "catalog:", diff --git a/apps/server-oauth2-mock/start-dev.mjs b/apps/server-oauth2-mock/start-dev.mjs deleted file mode 100644 index 3f9d0f985..000000000 --- a/apps/server-oauth2-mock/start-dev.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { spawn } from 'node:child_process'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; - -const childEnv = { ...process.env }; - -if (process.env.WORKTREE_NAME) { - const hostnames = getHostnames(); - childEnv.BASE_URL = buildPortlessUrl(hostnames.mockAuth); - // Override redirect URIs so portal-discovery picks up worktree-scoped URLs. - childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'); - childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'); -} - -const child = spawn('tsx', ['src/index.ts'], { - stdio: 'inherit', - env: childEnv, -}); - -forwardChildExit(child); diff --git a/apps/server-oauth2-mock/turbo.json b/apps/server-oauth2-mock/turbo.json index 2596ed01a..83fe0b4ba 100644 --- a/apps/server-oauth2-mock/turbo.json +++ b/apps/server-oauth2-mock/turbo.json @@ -14,7 +14,7 @@ "dependsOn": ["build"], "interruptible": true, "persistent": true, - "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json", "start-dev.mjs"] + "inputs": ["$TURBO_DEFAULT$", "../**/mock-oidc*.json"] } } } diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index f0cc73767..a4391587e 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -9,8 +9,8 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless ownercommunity.localhost --force node start-dev.mjs", - "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev": "pnpm exec portless ownercommunity.localhost --force cellix-local-dev vite --profile ui-community", + "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-community", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", @@ -39,6 +39,7 @@ "devDependencies": { "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", + "@cellix/local-dev": "workspace:*", "@chromatic-com/storybook": "^4.1.1", "@storybook/addon-a11y": "^9.1.3", "@storybook/addon-docs": "^9.1.3", diff --git a/apps/ui-community/start-dev.mjs b/apps/ui-community/start-dev.mjs deleted file mode 100644 index 233be3229..000000000 --- a/apps/ui-community/start-dev.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { spawn } from 'node:child_process'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; -import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; - -const childEnv = { ...process.env }; - -// Worktree-scoped overrides; plain `dev` leaves .env as the source of truth. -if (process.env.WORKTREE_NAME) { - const hostnames = getHostnames(); - childEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/community'); - childEnv.VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI = buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'); - childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); - childEnv.VITE_APP_UI_COMMUNITY_BASE_URL = buildPortlessUrl(hostnames.uiCommunity); -} - -const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); - -const child = spawn('vite', viteArgs, { - stdio: 'inherit', - env: childEnv, -}); - -forwardChildExit(child); diff --git a/apps/ui-community/turbo.json b/apps/ui-community/turbo.json index d05ef2c09..bcb802dd1 100644 --- a/apps/ui-community/turbo.json +++ b/apps/ui-community/turbo.json @@ -5,13 +5,13 @@ "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", ".env.*", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] }, "dev:worktree": { "dependsOn": ["^build"], "persistent": true, "interruptible": false, - "inputs": [".env", ".env.*", "package.json", "start-dev.mjs", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] + "inputs": [".env", ".env.*", "package.json", "vite.config.ts", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json"] } } } diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index f5c74fc0b..70158d368 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -9,8 +9,8 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless staff.ownercommunity.localhost --force node start-dev.mjs", - "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.mjs", + "dev": "pnpm exec portless staff.ownercommunity.localhost --force cellix-local-dev vite --profile ui-staff", + "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-staff", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", @@ -40,6 +40,7 @@ "devDependencies": { "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", + "@cellix/local-dev": "workspace:*", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^6.0.1", diff --git a/apps/ui-staff/start-dev.mjs b/apps/ui-staff/start-dev.mjs deleted file mode 100644 index c18f42dcf..000000000 --- a/apps/ui-staff/start-dev.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { spawn } from 'node:child_process'; -import { forwardChildExit } from '../../scripts/local-dev/dev-process-exit.mjs'; -import { buildPortlessUrl, getHostnames } from '../../scripts/local-dev/portless-hostnames.mjs'; -import { buildViteArgs } from '../../scripts/local-dev/vite-dev-args.mjs'; - -const childEnv = { ...process.env }; - -if (process.env.WORKTREE_NAME) { - const hostnames = getHostnames(); - childEnv.VITE_APP_UI_STAFF_AAD_AUTHORITY = buildPortlessUrl(hostnames.mockAuth, '/staff'); - childEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI = buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'); - childEnv.VITE_COMMON_API_ENDPOINT = buildPortlessUrl(hostnames.api, '/api/graphql'); -} - -const viteArgs = buildViteArgs({ host: process.env.HOST, port: process.env.PORT }); - -const child = spawn('vite', viteArgs, { - stdio: 'inherit', - env: childEnv, -}); - -forwardChildExit(child); diff --git a/biome.json b/biome.json index e14913384..5a0fc8b34 100644 --- a/biome.json +++ b/biome.json @@ -166,7 +166,7 @@ }, { "includes": ["**/src/**/*.ts"], "javascript": { "globals": [] } }, { - "includes": ["build-pipeline/scripts/**/*.{c|m}js", "scripts/local-dev/**/*.mjs"], + "includes": ["build-pipeline/scripts/**/*.{c|m}js", "packages/cellix/local-dev/bin/**/*.mjs"], "linter": { "rules": { "style": { "noCommonJs": "off" } } } }, { diff --git a/knip.json b/knip.json index e3de3d8c4..fa91a2bc3 100644 --- a/knip.json +++ b/knip.json @@ -2,23 +2,26 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "workspaces": { "apps/api": { - "entry": ["src/index.ts", "start-*.mjs"], - "project": ["src/**/*.ts", "*.mjs"], + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], "ignoreDependencies": ["azurite"] }, "apps/ui-community": { "entry": ["src/main.tsx"], "project": ["src/**/*.{ts,tsx}"], - "ignore": ["**/apollo-client-links.tsx"] + "ignore": ["**/apollo-client-links.tsx"], + "ignoreDependencies": ["@cellix/local-dev"] }, "apps/ui-staff": { "entry": ["src/main.tsx"], "project": ["src/**/*.{ts,tsx}"], - "ignore": [] + "ignore": [], + "ignoreDependencies": ["@cellix/local-dev"] }, "apps/docs": { "entry": ["src/**/*.{ts,tsx,js,jsx}"], - "project": ["src/**/*.{ts,tsx,js,jsx}"] + "project": ["src/**/*.{ts,tsx,js,jsx}"], + "ignoreDependencies": ["@cellix/local-dev"] }, "packages/cellix/*": { "project": ["src/**/*.ts"], @@ -90,25 +93,16 @@ "apps/server-oauth2-mock": { "entry": ["src/index.ts"], "project": ["src/**/*.ts"], - "ignoreDependencies": ["tsx"] + "ignoreDependencies": ["@cellix/local-dev", "tsx"] + }, + "apps/server-mongodb-memory-mock": { + "entry": ["src/index.ts"], + "project": ["src/**/*.ts"], + "ignoreDependencies": [] } }, "ignoreWorkspaces": ["packages/cellix/config-typescript"], - "ignore": [ - "build-pipeline/scripts/**", - "scripts/local-dev/**", - "**/fixtures/**", - "**/*.test.ts", - "**/*.spec.ts", - "**/*.stories.tsx", - "**/dist/**", - "**/coverage/**", - "**/__tests__/**", - "**/tests/**", - ".agents/**", - ".github/**", - "portless.config.cjs" - ], + "ignore": ["build-pipeline/scripts/**", "**/fixtures/**", "**/*.test.ts", "**/*.spec.ts", "**/*.stories.tsx", "**/dist/**", "**/coverage/**", "**/__tests__/**", "**/tests/**", ".agents/**", ".github/**", "portless.config.cjs"], "ignoreIssues": { "codegen.yml": ["unlisted"] }, diff --git a/packages/cellix/local-dev/.gitignore b/packages/cellix/local-dev/.gitignore new file mode 100644 index 000000000..c925c21d5 --- /dev/null +++ b/packages/cellix/local-dev/.gitignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md new file mode 100644 index 000000000..84dd87026 --- /dev/null +++ b/packages/cellix/local-dev/README.md @@ -0,0 +1,89 @@ +# @cellix/local-dev + +Shared local-development runtime for Cellix application packages. + +This package replaces duplicated `start-dev.*`, `start-mongo.*`, `start-azurite.*`, and root `scripts/local-dev/*` orchestration with one reusable package plus a small CLI. + +## What this package provides + +- Worktree-aware portless hostname derivation +- Worktree-aware MongoDB and Azurite port derivation +- API local-settings sync for normal and `e2e` modes +- Shared dev runners for: + - Vite apps + - Docusaurus docs + - Azure Functions local startup + - TSX-backed mock servers + - Azurite + +## Install + +In this monorepo, app packages consume the workspace package directly: + +```json +{ + "devDependencies": { + "@cellix/local-dev": "workspace:*" + } +} +``` + +## CLI usage + +```bash +cellix-local-dev vite --profile ui-community +cellix-local-dev vite --profile ui-staff +cellix-local-dev docusaurus +cellix-local-dev azure-functions +cellix-local-dev tsx --profile oauth2-mock +cellix-local-dev tsx --profile mongo-memory-mock +cellix-local-dev azurite +cellix-local-dev sync-api-local-settings +cellix-local-dev sync-api-local-settings e2e +``` + +## Example app scripts + +```json +{ + "scripts": { + "dev": "pnpm exec portless ownercommunity.localhost --force cellix-local-dev vite --profile ui-community", + "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-community" + } +} +``` + +```json +{ + "scripts": { + "sync-local-settings": "cellix-local-dev sync-api-local-settings", + "dev": "pnpm exec portless data-access.ownercommunity.localhost --force cellix-local-dev azure-functions", + "azurite": "cellix-local-dev azurite" + } +} +``` + +## Public API + +The package also exports the local-dev primitives for tests or scripted composition: + +- `resolveWorkspaceRoot` +- `resolvePortlessHostnames` +- `buildPortlessUrl` +- `buildViteArgs` +- `getWorktreePortOffset` +- `getMongoPort` +- `getAzuritePorts` +- `getAzuriteConnectionString` +- `getMongoConnectionString` +- `syncApiLocalSettings` +- `runViteDev` +- `runDocusaurusDev` +- `runAzureFunctionsDev` +- `runTsxDev` +- `runAzuriteDev` + +## Notes + +- The implementation is in TypeScript, but the package exposes a normal Node bin so consuming app scripts do not need to boot shared `.ts` files through `tsx`. +- The package derives the workspace root from the caller's current working directory, so app packages do not need to hardcode repo-relative paths. diff --git a/packages/cellix/local-dev/bin/cellix-local-dev.mjs b/packages/cellix/local-dev/bin/cellix-local-dev.mjs new file mode 100755 index 000000000..ffe9cc635 --- /dev/null +++ b/packages/cellix/local-dev/bin/cellix-local-dev.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const child = spawn(process.execPath, ['--experimental-strip-types', fileURLToPath(new URL('../src/bin.ts', import.meta.url)), ...process.argv.slice(2)], { + stdio: 'inherit', +}); + +child.on('error', (error) => { + console.error(`[local-dev] failed to start CLI: ${error.message}`); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + + process.exit(code ?? 1); +}); diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md new file mode 100644 index 000000000..b6883617f --- /dev/null +++ b/packages/cellix/local-dev/manifest.md @@ -0,0 +1,60 @@ +# manifest.md - @cellix/local-dev + +## Purpose + +Provide a single local-development runtime for Cellix app packages so app-level `dev`, `dev:worktree`, `azurite`, and local-settings scripts can stay configuration-shaped while the real orchestration lives in one reusable package. + +## Scope + +This package owns worktree-aware hostname and port derivation, portless URL building, API local-settings rewriting, child-process lifecycle handling, and the shared dev-runner orchestration for Vite, Docusaurus, Azure Functions, TSX-based mock servers, and Azurite. + +## Non-goals + +- Production runtime behavior +- Generic process management beyond Cellix local-development use cases +- Replacing app build/start scripts that are not part of the local-development workflow + +## Public API shape + +- `resolveWorkspaceRoot(options?)` +- `resolvePortlessHostnames(options?)`, `buildPortlessUrl(hostname, path?)`, `PORTLESS_PORT` +- `isE2E(env?)`, `buildViteArgs(options?)` +- `isGracefulInterruptExit(signal, code)`, `forwardChildExit(child)` +- `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)` +- `getAzuriteConnectionString(options?)`, `getMongoConnectionString(options?)` +- `syncApiLocalSettings(options?)` +- `runViteDev(profile, options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runTsxDev(profile, options?)`, `runAzuriteDev(options?)` + +## Core concepts + +- App packages should express local dev behavior as small parameter choices such as profile names or entry paths, not as duplicated process wiring. +- Worktree isolation is deterministic and should keep hostnames, MongoDB ports, and Azurite ports aligned across all participating apps. +- The package is allowed to know Cellix app profiles such as `ui-community`, `ui-staff`, `oauth2-mock`, and `mongo-memory-mock`, because those profiles are the stable consumer contract that replaces ad hoc app wrapper scripts. + +## Package boundaries + +- Keep CLI argument parsing, process spawning helpers, file-system helpers, and app profile definitions internal unless consumers outside this package need them directly. +- Do not leak app-relative path assumptions into consumers; the package must derive workspace paths from the caller's current working directory. +- Avoid widening the public surface with one-off helpers that only exist to support a single internal branch. + +## Dependencies / relationships + +- Downstream consumers in this monorepo: `@apps/api`, `@apps/docs`, `@apps/ui-community`, `@apps/ui-staff`, `@apps/server-oauth2-mock`, `@apps/server-mongodb-memory-mock` +- Consumed from package scripts through the `cellix-local-dev` bin and from tests through the TypeScript API + +## Testing strategy + +- Prefer public-entrypoint tests for hostname derivation, worktree port derivation, Vite arg building, connection-string patching, and API local-settings rewriting. +- Avoid tests that reach into internal CLI parsing or helper modules when a public function already proves the observable behavior. + +## Documentation obligations + +- Keep `README.md` focused on how app packages consume the package. +- Keep TSDoc aligned on public exports that define package behavior. +- Update this manifest when app profiles, public exports, or scope boundaries change. + +## Release-readiness standards + +- App packages should need only profile/entry configuration in `package.json` after consuming this package. +- Package build and package tests must pass, plus affected app builds/tests as justified by the migration. +- Any new app profile should be added deliberately and documented as part of the contract. diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json new file mode 100644 index 000000000..10c3d2454 --- /dev/null +++ b/packages/cellix/local-dev/package.json @@ -0,0 +1,42 @@ +{ + "name": "@cellix/local-dev", + "version": "1.0.0", + "description": "Shared local-development runner orchestration for Cellix applications", + "type": "module", + "files": [ + "dist", + "bin", + "src" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "bin": { + "cellix-local-dev": "bin/cellix-local-dev.mjs" + }, + "scripts": { + "prebuild": "pnpm run lint", + "build": "tsgo --build", + "clean": "rimraf dist", + "lint": "biome lint", + "format": "biome format --write", + "format:check": "biome format .", + "test": "vitest run --silent --reporter=dot", + "test:coverage": "vitest run --coverage --silent --reporter=dot", + "test:watch": "vitest" + }, + "devDependencies": { + "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", + "@types/node": "catalog:", + "@vitest/coverage-istanbul": "catalog:", + "rimraf": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/cellix/local-dev/src/api-local-settings.ts b/packages/cellix/local-dev/src/api-local-settings.ts new file mode 100644 index 000000000..f91a2a82b --- /dev/null +++ b/packages/cellix/local-dev/src/api-local-settings.ts @@ -0,0 +1,85 @@ +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { buildPortlessUrl, resolvePortlessHostnames } from './hostnames.ts'; +import { getAzuriteConnectionString } from './worktree-ports.ts'; + +interface ApiLocalSettingsDocument { + Values?: Record; +} + +interface SyncApiLocalSettingsOptions { + appDir?: string; + workspaceRoot?: string; + env?: NodeJS.ProcessEnv; + mode?: 'e2e'; +} + +/** + * Applies the shared Cellix e2e and worktree overrides to an API + * `local.settings.json` document. + */ +export function applyApiLocalSettingsOverrides(settings: ApiLocalSettingsDocument, options: Pick = {}): ApiLocalSettingsDocument { + const env = options.env ?? process.env; + const values = { ...(settings.Values ?? {}) }; + + if (env['WORKTREE_NAME']) { + const hostnames = resolvePortlessHostnames({ + ...(options.workspaceRoot ? { startDir: options.workspaceRoot } : {}), + env, + }); + values['ACCOUNT_PORTAL_OIDC_ISSUER'] = buildPortlessUrl(hostnames.mockAuth, '/community'); + values['ACCOUNT_PORTAL_OIDC_ENDPOINT'] = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); + values['STAFF_PORTAL_OIDC_ISSUER'] = buildPortlessUrl(hostnames.mockAuth, '/staff'); + values['STAFF_PORTAL_OIDC_ENDPOINT'] = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); + const azurite = getAzuriteConnectionString({ + ...(options.workspaceRoot ? { startDir: options.workspaceRoot } : {}), + env, + values, + }); + values['AZURE_STORAGE_CONNECTION_STRING'] = azurite; + values['AzureWebJobsStorage'] = azurite; + } + + if (env['COSMOSDB_CONNECTION_STRING']) { + values['COSMOSDB_CONNECTION_STRING'] = env['COSMOSDB_CONNECTION_STRING']; + } + + settings.Values = values; + return settings; +} + +/** + * Syncs `apps/api/deploy/local.settings.json` from the local or e2e source file. + */ +export function syncApiLocalSettings(options: SyncApiLocalSettingsOptions = {}): void { + const env = options.env ?? process.env; + const appDir = path.resolve(options.appDir ?? process.cwd()); + const mode = options.mode ?? (['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()) ? 'e2e' : undefined); + const localSettingsPath = path.join(appDir, 'local.settings.json'); + const e2eLocalSettingsPath = path.join(appDir, 'local-settings.e2e.json'); + const targetPath = path.join(appDir, 'deploy', 'local.settings.json'); + + mkdirSync(path.dirname(targetPath), { recursive: true }); + + if (!mode) { + if (existsSync(localSettingsPath)) { + copyFileSync(localSettingsPath, targetPath); + } + return; + } + + if (mode !== 'e2e') { + throw new Error(`[local-dev] Invalid mode: expected one of e2e, received "${mode}"`); + } + + if (!existsSync(e2eLocalSettingsPath)) { + throw new Error(`[local-dev] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`); + } + + const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf8')) as ApiLocalSettingsDocument; + applyApiLocalSettingsOverrides(settings, { + workspaceRoot: options.workspaceRoot ?? appDir, + env, + }); + writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`); +} diff --git a/packages/cellix/local-dev/src/bin.ts b/packages/cellix/local-dev/src/bin.ts new file mode 100644 index 000000000..e6bbb3f23 --- /dev/null +++ b/packages/cellix/local-dev/src/bin.ts @@ -0,0 +1,7 @@ +import { runCli } from './cli.ts'; + +const exitCode = runCli(process.argv.slice(2)); + +if (typeof exitCode === 'number' && exitCode !== 0) { + process.exit(exitCode); +} diff --git a/packages/cellix/local-dev/src/cli.ts b/packages/cellix/local-dev/src/cli.ts new file mode 100644 index 000000000..1d9e1da4c --- /dev/null +++ b/packages/cellix/local-dev/src/cli.ts @@ -0,0 +1,121 @@ +import { syncApiLocalSettings } from './api-local-settings.ts'; +import { runAzureFunctionsDev, runAzuriteDev, runDocusaurusDev, runTsxDev, runViteDev, type TsxDevProfile, type ViteDevProfile } from './runners.ts'; + +type CliCommand = 'vite' | 'docusaurus' | 'azure-functions' | 'tsx' | 'azurite' | 'sync-api-local-settings'; + +type FlagMap = Record; + +function parseFlags(args: string[]): { flags: FlagMap; positionals: string[] } { + const flags: FlagMap = {}; + const positionals: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg) { + continue; + } + if (arg === '--help' || arg === '-h') { + flags['help'] = 'true'; + continue; + } + + if (arg.startsWith('--')) { + const key = arg.slice(2); + const value = args[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`[local-dev] Missing value for --${key}`); + } + flags[key] = value; + index += 1; + continue; + } + + positionals.push(arg); + } + + return { flags, positionals }; +} + +function usage(): string { + return [ + 'Usage:', + ' cellix-local-dev vite --profile ', + ' cellix-local-dev docusaurus', + ' cellix-local-dev azure-functions', + ' cellix-local-dev tsx --profile [--entry src/index.ts]', + ' cellix-local-dev azurite', + ' cellix-local-dev sync-api-local-settings [e2e]', + ].join('\n'); +} + +function asViteProfile(value: string | undefined): ViteDevProfile { + if (value === 'ui-community' || value === 'ui-staff') { + return value; + } + throw new Error(`[local-dev] Invalid vite profile "${value ?? ''}"`); +} + +function asTsxProfile(value: string | undefined): TsxDevProfile { + if (value === 'oauth2-mock' || value === 'mongo-memory-mock') { + return value; + } + throw new Error(`[local-dev] Invalid tsx profile "${value ?? ''}"`); +} + +/** + * Runs the `cellix-local-dev` CLI. + */ +export function runCli(argv = process.argv.slice(2)): number { + const [command, ...rest] = argv as [CliCommand | undefined, ...string[]]; + + if (!command) { + console.error(usage()); + return 1; + } + + try { + const { flags, positionals } = parseFlags(rest); + if (flags['help'] === 'true') { + console.log(usage()); + return 0; + } + + switch (command) { + case 'vite': + runViteDev(asViteProfile(flags['profile'])); + return 0; + case 'docusaurus': + runDocusaurusDev(); + return 0; + case 'azure-functions': + runAzureFunctionsDev(); + return 0; + case 'tsx': + runTsxDev(asTsxProfile(flags['profile']), { + ...(flags['entry'] ? { entry: flags['entry'] } : {}), + }); + return 0; + case 'azurite': + runAzuriteDev(); + return 0; + case 'sync-api-local-settings': + if (positionals[0] && positionals[0] !== 'e2e') { + throw new Error(`[local-dev] Invalid sync-api-local-settings mode "${positionals[0]}"`); + } + syncApiLocalSettings( + positionals[0] === 'e2e' + ? { + mode: 'e2e', + } + : {}, + ); + return 0; + default: + console.error(usage()); + return 1; + } + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + return 1; + } +} diff --git a/packages/cellix/local-dev/src/dev-process.ts b/packages/cellix/local-dev/src/dev-process.ts new file mode 100644 index 000000000..ec9394fca --- /dev/null +++ b/packages/cellix/local-dev/src/dev-process.ts @@ -0,0 +1,24 @@ +import type { ChildProcess } from 'node:child_process'; + +/** + * Returns true for interrupt-style exits that should be treated as graceful + * shutdowns when Turbo or the terminal stops a persistent dev process. + */ +export function isGracefulInterruptExit(signal: NodeJS.Signals | null | undefined, code: number | null | undefined): boolean { + return signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT' || code === 130 || code === 143; +} + +/** + * Forwards a spawned child process exit code back to the parent process while + * treating common interrupt exits as a successful shutdown. + */ +export function forwardChildExit(child: ChildProcess): void { + child.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + process.exitCode = 0; + return; + } + + process.exitCode = code ?? 1; + }); +} diff --git a/packages/cellix/local-dev/src/hostnames.ts b/packages/cellix/local-dev/src/hostnames.ts new file mode 100644 index 000000000..1afa4c8ae --- /dev/null +++ b/packages/cellix/local-dev/src/hostnames.ts @@ -0,0 +1,91 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; + +export interface PortlessHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +export type PortlessHostnameKey = keyof PortlessHostnames; + +interface ResolvePortlessHostnamesOptions extends ResolveWorkspaceRootOptions { + env?: NodeJS.ProcessEnv; +} + +type DotEnvValues = Record; + +export const PORTLESS_PORT = 1355; + +function readDotEnv(filePath: string): DotEnvValues { + if (!existsSync(filePath)) return {}; + + const result: DotEnvValues = {}; + for (const line of readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + + return result; +} + +function hostnameFrom(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +function hostnameFor(key: string, values: DotEnvValues, env: NodeJS.ProcessEnv): string | null { + return hostnameFrom(env[key] ?? values[key] ?? ''); +} + +function applyWorktreeSuffix(hostname: string, worktreeName: string | undefined): string { + if (!worktreeName) return hostname; + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} + +/** + * Resolves the local hostnames shared by the main Cellix dev applications. + */ +export function resolvePortlessHostnames(options: ResolvePortlessHostnamesOptions = {}): PortlessHostnames { + const env = options.env ?? process.env; + const workspaceRoot = resolveWorkspaceRoot(options); + const uiEnv = readDotEnv(path.join(workspaceRoot, 'apps', 'ui-community', '.env')); + const staffEnv = readDotEnv(path.join(workspaceRoot, 'apps', 'ui-staff', '.env')); + + const uiCommunity = hostnameFor('VITE_APP_UI_COMMUNITY_BASE_URL', uiEnv, env); + const api = hostnameFor('VITE_COMMON_API_ENDPOINT', uiEnv, env); + const mockAuth = hostnameFor('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiEnv, env); + const uiStaff = hostnameFor('VITE_APP_UI_STAFF_AAD_REDIRECT_URI', staffEnv, env); + + if (!uiCommunity || !api || !mockAuth || !uiStaff) { + throw new Error('[local-dev] Could not derive all portless hostnames. Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); + } + + const worktreeName = env['WORKTREE_NAME']; + const docs = `docs.${uiCommunity}`; + + return { + uiCommunity: applyWorktreeSuffix(uiCommunity, worktreeName), + uiStaff: applyWorktreeSuffix(uiStaff, worktreeName), + api: applyWorktreeSuffix(api, worktreeName), + mockAuth: applyWorktreeSuffix(mockAuth, worktreeName), + docs: applyWorktreeSuffix(docs, worktreeName), + }; +} + +/** + * Builds a portless-proxied HTTPS URL for a hostname and optional path. + */ +export function buildPortlessUrl(hostname: string, relativePath = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${relativePath}`; +} diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts new file mode 100644 index 000000000..d0bfe34c1 --- /dev/null +++ b/packages/cellix/local-dev/src/index.test.ts @@ -0,0 +1,137 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { + applyApiLocalSettingsOverrides, + buildPortlessUrl, + buildViteArgs, + getAzuriteConnectionString, + getAzuritePorts, + getMongoConnectionString, + getMongoPort, + getWorktreePortOffset, + PORTLESS_PORT, + resolvePortlessHostnames, + resolveWorkspaceRoot, +} from '@cellix/local-dev'; +import { describe, expect, it } from 'vitest'; + +function createWorkspaceFixture(): string { + const root = mkdtempSync(path.join(tmpdir(), 'cellix-local-dev-')); + writeFileSync(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - "apps/*"\n'); + mkdirSync(path.join(root, 'apps', 'ui-community'), { recursive: true }); + mkdirSync(path.join(root, 'apps', 'ui-staff'), { recursive: true }); + mkdirSync(path.join(root, 'apps', 'api'), { recursive: true }); + + writeFileSync( + path.join(root, 'apps', 'ui-community', '.env'), + [ + 'VITE_APP_UI_COMMUNITY_BASE_URL=https://ownercommunity.localhost:1355', + 'VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql', + 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/community', + ].join('\n'), + ); + writeFileSync(path.join(root, 'apps', 'ui-staff', '.env'), 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI=https://staff.ownercommunity.localhost:1355/auth-redirect\n'); + writeFileSync( + path.join(root, 'apps', 'api', 'local.settings.json'), + JSON.stringify( + { + Values: { + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/test?replicaSet=rs0', + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + }, + }, + null, + 2, + ), + ); + + return root; +} + +describe('@cellix/local-dev', () => { + it('resolves the workspace root from a nested directory', () => { + const workspaceRoot = createWorkspaceFixture(); + const nestedDir = path.join(workspaceRoot, 'apps', 'ui-community'); + + expect(resolveWorkspaceRoot({ startDir: nestedDir })).toBe(workspaceRoot); + }); + + it('derives shared hostnames and applies the worktree suffix', () => { + const workspaceRoot = createWorkspaceFixture(); + + expect( + resolvePortlessHostnames({ + startDir: path.join(workspaceRoot, 'apps', 'ui-community'), + env: { WORKTREE_NAME: 'feature-123' }, + }), + ).toEqual({ + uiCommunity: 'ownercommunity.feature-123.localhost', + uiStaff: 'staff.ownercommunity.feature-123.localhost', + api: 'data-access.ownercommunity.feature-123.localhost', + mockAuth: 'mock-auth.ownercommunity.feature-123.localhost', + docs: 'docs.ownercommunity.feature-123.localhost', + }); + expect(buildPortlessUrl('ownercommunity.localhost')).toBe(`https://ownercommunity.localhost:${PORTLESS_PORT}`); + }); + + it('builds shared Vite args including e2e mode', () => { + expect( + buildViteArgs({ + host: '0.0.0.0', + port: '4444', + env: { E2E: 'true' }, + }), + ).toEqual(['--host', '0.0.0.0', '--port', '4444', '--mode', 'e2e']); + }); + + it('derives deterministic worktree ports and connection strings', () => { + const workspaceRoot = createWorkspaceFixture(); + const env = { WORKTREE_NAME: 'feature-123' }; + + expect(getWorktreePortOffset('feature-123')).toBeGreaterThanOrEqual(100); + expect(getMongoPort('feature-123')).toBe(50000 + getWorktreePortOffset('feature-123')); + expect(getAzuritePorts('feature-123')).toEqual({ + blob: 10000 + getWorktreePortOffset('feature-123'), + queue: 10001 + getWorktreePortOffset('feature-123'), + table: 10002 + getWorktreePortOffset('feature-123'), + }); + expect( + getMongoConnectionString({ + startDir: path.join(workspaceRoot, 'apps', 'api'), + env, + }), + ).toContain(`:${getMongoPort('feature-123')}/test?replicaSet=rs0`); + expect( + getAzuriteConnectionString({ + startDir: path.join(workspaceRoot, 'apps', 'api'), + env, + }), + ).toContain(`BlobEndpoint=http://127.0.0.1:${getAzuritePorts('feature-123').blob}/devstoreaccount1`); + }); + + it('applies worktree and runtime overrides to API local settings', () => { + const workspaceRoot = createWorkspaceFixture(); + const settings = { + Values: { + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', + }, + }; + + const updated = applyApiLocalSettingsOverrides(settings, { + workspaceRoot, + env: { + WORKTREE_NAME: 'feature-123', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:61234/override?replicaSet=rs0', + }, + }); + + expect(updated.Values?.['ACCOUNT_PORTAL_OIDC_ISSUER']).toBe('https://mock-auth.ownercommunity.feature-123.localhost:1355/community'); + expect(updated.Values?.['STAFF_PORTAL_OIDC_ENDPOINT']).toBe('https://mock-auth.ownercommunity.feature-123.localhost:1355/staff/.well-known/jwks.json'); + expect(updated.Values?.['COSMOSDB_CONNECTION_STRING']).toBe('mongodb://127.0.0.1:61234/override?replicaSet=rs0'); + expect(updated.Values?.['AZURE_STORAGE_CONNECTION_STRING']).not.toBe('UseDevelopmentStorage=true'); + }); +}); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts new file mode 100644 index 000000000..2cdaacbbf --- /dev/null +++ b/packages/cellix/local-dev/src/index.ts @@ -0,0 +1,31 @@ +export { applyApiLocalSettingsOverrides, syncApiLocalSettings } from './api-local-settings.ts'; +export { runCli } from './cli.ts'; +export { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; +export { + buildPortlessUrl, + PORTLESS_PORT, + type PortlessHostnameKey, + type PortlessHostnames, + resolvePortlessHostnames, +} from './hostnames.ts'; +export { + type RunnerOptions, + runAzureFunctionsDev, + runAzuriteDev, + runDocusaurusDev, + runTsxDev, + runViteDev, + type TsxDevProfile, + type ViteDevProfile, +} from './runners.ts'; +export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite.ts'; +export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; +export { + type AzuritePorts, + type ConnectionStringOptions, + getAzuriteConnectionString, + getAzuritePorts, + getMongoConnectionString, + getMongoPort, + getWorktreePortOffset, +} from './worktree-ports.ts'; diff --git a/packages/cellix/local-dev/src/runners.ts b/packages/cellix/local-dev/src/runners.ts new file mode 100644 index 000000000..aecd430ac --- /dev/null +++ b/packages/cellix/local-dev/src/runners.ts @@ -0,0 +1,243 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import os from 'node:os'; +import path from 'node:path'; +import { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; +import { buildPortlessUrl, type PortlessHostnameKey, resolvePortlessHostnames } from './hostnames.ts'; +import { buildViteArgs } from './vite.ts'; +import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; +import { getAzuriteConnectionString, getAzuritePorts, getMongoConnectionString, getMongoPort } from './worktree-ports.ts'; + +export type ViteDevProfile = 'ui-community' | 'ui-staff'; +export type TsxDevProfile = 'oauth2-mock' | 'mongo-memory-mock'; + +export interface RunnerOptions extends ResolveWorkspaceRootOptions { + env?: NodeJS.ProcessEnv; +} + +interface TsxRunnerOptions extends RunnerOptions { + entry?: string; +} + +type OverrideSpec = Record; + +function spawnInherited(command: string, args: string[], options: { env?: NodeJS.ProcessEnv } = {}): ChildProcess { + return spawn(command, args, { + stdio: 'inherit', + env: options.env, + }); +} + +function applyPortlessEnvOverrides(childEnv: NodeJS.ProcessEnv, overrideSpec: OverrideSpec, options: RunnerOptions, settings: { preserveExisting?: boolean } = {}): void { + const env = options.env ?? process.env; + if (!env['WORKTREE_NAME']) return; + + const hostnames = resolvePortlessHostnames(options); + for (const [key, target] of Object.entries(overrideSpec)) { + if (settings.preserveExisting && childEnv[key]) { + continue; + } + + childEnv[key] = buildPortlessUrl(hostnames[target.hostname], target.path ?? ''); + } +} + +/** + * Starts a Vite dev process using one of the shared Cellix UI profiles. + */ +export function runViteDev(profile: ViteDevProfile, options: RunnerOptions = {}): ChildProcess { + const env = options.env ?? process.env; + const childEnv = { ...env }; + + if (profile === 'ui-community') { + applyPortlessEnvOverrides( + childEnv, + { + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: { + hostname: 'mockAuth', + path: '/community', + }, + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: { + hostname: 'uiCommunity', + path: '/auth-redirect', + }, + VITE_COMMON_API_ENDPOINT: { hostname: 'api', path: '/api/graphql' }, + VITE_APP_UI_COMMUNITY_BASE_URL: { hostname: 'uiCommunity' }, + }, + options, + ); + } else { + applyPortlessEnvOverrides( + childEnv, + { + VITE_APP_UI_STAFF_AAD_AUTHORITY: { hostname: 'mockAuth', path: '/staff' }, + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: { + hostname: 'uiStaff', + path: '/auth-redirect', + }, + VITE_COMMON_API_ENDPOINT: { hostname: 'api', path: '/api/graphql' }, + }, + options, + ); + } + + const child = spawnInherited( + 'vite', + buildViteArgs({ + ...(env['HOST'] ? { host: env['HOST'] } : {}), + ...(env['PORT'] ? { port: env['PORT'] } : {}), + env, + }), + { env: childEnv }, + ); + forwardChildExit(child); + return child; +} + +/** + * Starts the Docusaurus dev server with the shared local-dev defaults. + */ +export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { + const env = options.env ?? process.env; + const child = spawnInherited('docusaurus', ['start', '--port', env['PORT'] ?? '3001', '--host', '127.0.0.1', '--no-open']); + forwardChildExit(child); + return child; +} + +/** + * Starts the API Azure Functions process with shared CA, CORS, and worktree overrides. + */ +export function runAzureFunctionsDev(options: RunnerOptions = {}): ChildProcess { + const env = options.env ?? process.env; + const envPort = env['PORT']; + + if (!envPort) { + throw new Error('[local-dev] PORT environment variable is not set. Start this command through portless.'); + } + + const childEnv: NodeJS.ProcessEnv = { + ...env, + NODE_EXTRA_CA_CERTS: env['PORTLESS_CA_PATH'] ?? path.join(os.homedir(), '.portless', 'ca.pem'), + NODE_OPTIONS: `${env['NODE_OPTIONS'] ?? ''} --use-system-ca`.trim(), + }; + + if (env['WORKTREE_NAME']) { + applyPortlessEnvOverrides( + childEnv, + { + ACCOUNT_PORTAL_OIDC_ISSUER: { hostname: 'mockAuth', path: '/community' }, + ACCOUNT_PORTAL_OIDC_ENDPOINT: { + hostname: 'mockAuth', + path: '/community/.well-known/jwks.json', + }, + STAFF_PORTAL_OIDC_ISSUER: { hostname: 'mockAuth', path: '/staff' }, + STAFF_PORTAL_OIDC_ENDPOINT: { + hostname: 'mockAuth', + path: '/staff/.well-known/jwks.json', + }, + }, + options, + { preserveExisting: true }, + ); + childEnv['COSMOSDB_CONNECTION_STRING'] ??= getMongoConnectionString(options); + childEnv['AZURE_STORAGE_CONNECTION_STRING'] ??= getAzuriteConnectionString(options); + childEnv['AzureWebJobsStorage'] ??= getAzuriteConnectionString(options); + childEnv['languageWorkers__node__arguments'] ??= ''; + } + + const child = spawnInherited('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { env: childEnv }); + forwardChildExit(child); + return child; +} + +/** + * Starts a TSX-backed mock service using one of the shared Cellix profiles. + */ +export function runTsxDev(profile: TsxDevProfile, options: TsxRunnerOptions = {}): ChildProcess { + const env = options.env ?? process.env; + const childEnv: NodeJS.ProcessEnv = { ...env }; + + if (profile === 'oauth2-mock') { + applyPortlessEnvOverrides( + childEnv, + { + BASE_URL: { hostname: 'mockAuth' }, + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: { + hostname: 'uiCommunity', + path: '/auth-redirect', + }, + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: { + hostname: 'uiStaff', + path: '/auth-redirect', + }, + }, + options, + ); + } else if (profile === 'mongo-memory-mock') { + childEnv['PORT'] = String(getMongoPort(env['WORKTREE_NAME'])); + } + + const child = spawnInherited('tsx', [options.entry ?? 'src/index.ts'], { env: childEnv }); + forwardChildExit(child); + return child; +} + +/** + * Starts the three Azurite worker processes with worktree-scoped ports and storage paths. + */ +export function runAzuriteDev(options: RunnerOptions = {}): ChildProcess[] { + const env = options.env ?? process.env; + const workspaceRoot = resolveWorkspaceRoot(options); + const ports = getAzuritePorts(env['WORKTREE_NAME']); + const storageSuffix = env['WORKTREE_NAME'] ? `-${env['WORKTREE_NAME']}` : ''; + + const procSpecs: Array<[string, string[]]> = [ + ['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', path.join(workspaceRoot, `__blobstorage__${storageSuffix}`)]], + ['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', path.join(workspaceRoot, `__queuestorage__${storageSuffix}`)]], + ['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', path.join(workspaceRoot, `__tablestorage__${storageSuffix}`)]], + ]; + + const procs = procSpecs.map(([command, args]) => { + const proc = spawnInherited(command, args); + proc.on('error', (error) => { + console.error(`[azurite] failed to start ${command}: ${error.message}`); + for (const runningProc of procs) { + runningProc.kill(); + } + process.exit(1); + }); + return proc; + }); + + console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`); + + let exited = 0; + for (const proc of procs) { + proc.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + if (++exited === procs.length) { + process.exit(0); + } + return; + } + + console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); + for (const runningProc of procs) { + runningProc.kill(); + } + process.exit(code ?? 1); + }); + } + + process.on('SIGINT', () => { + for (const proc of procs) { + proc.kill('SIGINT'); + } + }); + process.on('SIGTERM', () => { + for (const proc of procs) { + proc.kill('SIGTERM'); + } + }); + + return procs; +} diff --git a/packages/cellix/local-dev/src/vite.ts b/packages/cellix/local-dev/src/vite.ts new file mode 100644 index 000000000..7944136f1 --- /dev/null +++ b/packages/cellix/local-dev/src/vite.ts @@ -0,0 +1,31 @@ +export interface BuildViteArgsOptions { + host?: string; + port?: string; + env?: NodeJS.ProcessEnv; +} + +/** + * Returns true when the current process is running in an e2e-oriented mode. + */ +export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { + return ['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()); +} + +/** + * Builds the shared argument list for Vite dev startup across Cellix apps. + */ +export function buildViteArgs(options: BuildViteArgsOptions = {}): string[] { + const { host = '127.0.0.1', port, env = process.env } = options; + const args = ['--host', host]; + + if (port) { + args.push('--port', port); + } + + const viteMode = env['E2E_VITE_MODE'] ?? (isE2E(env) || env['TF_BUILD'] ? 'e2e' : undefined); + if (viteMode) { + args.push('--mode', viteMode); + } + + return args; +} diff --git a/packages/cellix/local-dev/src/workspace.ts b/packages/cellix/local-dev/src/workspace.ts new file mode 100644 index 000000000..19db913ff --- /dev/null +++ b/packages/cellix/local-dev/src/workspace.ts @@ -0,0 +1,26 @@ +import { existsSync } from 'node:fs'; +import path from 'node:path'; + +export interface ResolveWorkspaceRootOptions { + startDir?: string; +} + +/** + * Finds the Cellix workspace root by walking upward until `pnpm-workspace.yaml` + * is present. + */ +export function resolveWorkspaceRoot(options: ResolveWorkspaceRootOptions = {}): string { + let currentDir = path.resolve(options.startDir ?? process.cwd()); + + for (;;) { + if (existsSync(path.join(currentDir, 'pnpm-workspace.yaml'))) { + return currentDir; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + throw new Error(`[local-dev] Could not find pnpm-workspace.yaml above ${options.startDir ?? process.cwd()}`); + } + currentDir = parentDir; + } +} diff --git a/packages/cellix/local-dev/src/worktree-ports.ts b/packages/cellix/local-dev/src/worktree-ports.ts new file mode 100644 index 000000000..d9b4ff806 --- /dev/null +++ b/packages/cellix/local-dev/src/worktree-ports.ts @@ -0,0 +1,107 @@ +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; + +type SettingsValues = Record; + +export interface AzuritePorts { + blob: number; + queue: number; + table: number; +} + +export interface ConnectionStringOptions extends ResolveWorkspaceRootOptions { + env?: NodeJS.ProcessEnv; + values?: SettingsValues; +} + +function readApiLocalSettingsValues(workspaceRoot: string): SettingsValues { + const candidatePaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; + + for (const settingsPath of candidatePaths) { + if (!existsSync(settingsPath)) continue; + const settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as { + Values?: SettingsValues; + }; + return settings.Values ?? {}; + } + + return {}; +} + +function getSetting(name: string, options: ConnectionStringOptions, workspaceRoot: string): string | undefined { + return options.env?.[name] ?? options.values?.[name] ?? readApiLocalSettingsValues(workspaceRoot)[name]; +} + +/** + * Returns a deterministic worktree port offset in increments of 100. + */ +export function getWorktreePortOffset(worktreeName = process.env['WORKTREE_NAME']): number { + if (!worktreeName) return 0; + + let hash = 0; + for (const char of worktreeName) { + hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0; + } + + return ((Math.abs(hash) % 49) + 1) * 100; +} + +/** + * Returns the MongoDB port for the current worktree. + */ +export function getMongoPort(worktreeName = process.env['WORKTREE_NAME']): number { + return 50000 + getWorktreePortOffset(worktreeName); +} + +/** + * Returns the Azurite ports for the current worktree. + */ +export function getAzuritePorts(worktreeName = process.env['WORKTREE_NAME']): AzuritePorts { + const offset = getWorktreePortOffset(worktreeName); + + return { + blob: 10000 + offset, + queue: 10001 + offset, + table: 10002 + offset, + }; +} + +/** + * Returns the Azurite connection string for the current worktree. + */ +export function getAzuriteConnectionString(options: ConnectionStringOptions = {}): string { + const workspaceRoot = resolveWorkspaceRoot(options); + const ports = getAzuritePorts(options.env?.['WORKTREE_NAME'] ?? process.env['WORKTREE_NAME']); + if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; + + const accountName = getSetting('STORAGE_ACCOUNT_NAME', options, workspaceRoot); + const accountKey = getSetting('STORAGE_ACCOUNT_KEY', options, workspaceRoot); + if (!accountName || !accountKey) { + throw new Error('[local-dev] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); + } + + return [ + 'DefaultEndpointsProtocol=http', + `AccountName=${accountName}`, + `AccountKey=${accountKey}`, + `BlobEndpoint=http://127.0.0.1:${ports.blob}/${accountName}`, + `QueueEndpoint=http://127.0.0.1:${ports.queue}/${accountName}`, + `TableEndpoint=http://127.0.0.1:${ports.table}/${accountName}`, + ].join(';'); +} + +/** + * Reads the API Mongo connection string and patches in the worktree-specific port. + */ +export function getMongoConnectionString(options: ConnectionStringOptions = {}): string { + const workspaceRoot = resolveWorkspaceRoot(options); + const base = getSetting('COSMOSDB_CONNECTION_STRING', options, workspaceRoot); + if (!base) { + throw new Error('[local-dev] COSMOSDB_CONNECTION_STRING must be set'); + } + + const url = new URL(base); + url.port = String(getMongoPort(options.env?.['WORKTREE_NAME'] ?? process.env['WORKTREE_NAME'])); + return url.toString(); +} diff --git a/packages/cellix/local-dev/tsconfig.json b/packages/cellix/local-dev/tsconfig.json new file mode 100644 index 000000000..3b79c6fd1 --- /dev/null +++ b/packages/cellix/local-dev/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@cellix/config-typescript/node", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/cellix/local-dev/tsconfig.vitest.json b/packages/cellix/local-dev/tsconfig.vitest.json new file mode 100644 index 000000000..0d3bc7fca --- /dev/null +++ b/packages/cellix/local-dev/tsconfig.vitest.json @@ -0,0 +1,9 @@ +{ + "extends": ["./tsconfig.json", "@cellix/config-typescript/vitest"], + "compilerOptions": { + "paths": { + "@cellix/local-dev": ["./src/index.ts"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/cellix/local-dev/vitest.config.ts b/packages/cellix/local-dev/vitest.config.ts new file mode 100644 index 000000000..2c2d02b84 --- /dev/null +++ b/packages/cellix/local-dev/vitest.config.ts @@ -0,0 +1,18 @@ +import { join } from 'node:path'; +import { getDirnameFromImportMetaUrl, nodeConfig } from '@cellix/config-vitest'; +import { mergeConfig } from 'vitest/config'; + +const dirname = getDirnameFromImportMetaUrl(import.meta.url); + +export default mergeConfig(nodeConfig, { + test: { + typecheck: { + tsconfig: './tsconfig.vitest.json', + }, + }, + resolve: { + alias: { + '@cellix/local-dev': join(dirname, 'src/index.ts'), + }, + }, +}); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts index 51f05e970..1bb126c2f 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { ProcessTestServer } from './index.ts'; async function waitUntil(predicate: () => boolean, timeoutMs = 2_000): Promise { @@ -12,6 +12,10 @@ async function waitUntil(predicate: () => boolean, timeoutMs = 2_000): Promise { + afterEach(() => { + vi.restoreAllMocks(); + }); + it('starts a process and trusts the ready marker when probing is disabled', async () => { const server = new ProcessTestServer({ serverName: 'marker-only server', @@ -50,4 +54,29 @@ describe('ProcessTestServer', () => { expect(server.isRunning()).toBe(false); }); + + it('reuses an already-running server when a replacement exits with EADDRINUSE', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response('ok', { status: 200 })), + ); + + const server = new ProcessTestServer({ + serverName: 'reusable-existing server', + executable: process.execPath, + spawnArgs: ['-e', "console.error('Error: listen EADDRINUSE: address already in use 127.0.0.1:4965'); process.exit(1)"], + cwd: process.cwd(), + readyMarker: 'READY', + getUrl: () => 'http://127.0.0.1:4965', + isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), + shutdownTimeoutMs: 500, + }); + + try { + await server.start(); + expect(server.isRunning()).toBe(false); + } finally { + await server.stop(); + } + }); }); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.ts index 6491e0732..da2f6ddc6 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.ts @@ -223,7 +223,13 @@ export class ProcessTestServer implements TestServer { clearTimeout(timeout); if (this.options.isReusableExit?.(stderrOutput)) { - resolve(); + this.waitForProbeReady(startupDeadline, startupTimeout) + .then(() => { + resolve(); + }) + .catch((error: unknown) => { + reject(error); + }); return; } diff --git a/packages/ocom-verification/e2e-tests/src/test-server-factories.ts b/packages/ocom-verification/e2e-tests/src/test-server-factories.ts index 83d72cb67..be75b270e 100644 --- a/packages/ocom-verification/e2e-tests/src/test-server-factories.ts +++ b/packages/ocom-verification/e2e-tests/src/test-server-factories.ts @@ -1,4 +1,3 @@ -import { join } from 'node:path'; import { ProcessTestServer } from '@cellix/serenity-framework/servers'; import { getAzuritePorts } from '@ocom-verification/verification-shared/environment'; import { appPaths } from './shared/environment/app-paths.ts'; @@ -47,19 +46,14 @@ export function createTestApiServer(getMongoConnectionString: () => string): Pro export function createTestAzuriteServer(): ProcessTestServer { return new ProcessTestServer({ cwd: appPaths.apiDir, - executable: 'node', - extraEnv: () => { - const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); - const { PATH: pathValue = '' } = process.env; - return { PATH: `${binDir}:${pathValue}` }; - }, + executable: 'pnpm', getUrl: () => `http://127.0.0.1:${getAzuritePorts().blob}`, isAlreadyRunning: async () => false, isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), probe: false, readyMarker: '[azurite] started', serverName: 'TestAzuriteServer', - spawnArgs: ['start-azurite.mjs'], + spawnArgs: ['run', 'azurite'], }); } @@ -68,6 +62,7 @@ export function createTestOAuth2Server(): ProcessTestServer { cwd: appPaths.oauth2MockDir, executable: 'pnpm', getUrl: () => mockOidcIssuer, + isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), probe: { url: mockOidcEndpoint, }, diff --git a/packages/ocom-verification/e2e-tests/turbo.json b/packages/ocom-verification/e2e-tests/turbo.json index 5a7a5f821..de20c2818 100644 --- a/packages/ocom-verification/e2e-tests/turbo.json +++ b/packages/ocom-verification/e2e-tests/turbo.json @@ -7,10 +7,9 @@ "src/**/*.ts", "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", - "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", "$TURBO_ROOT$/apps/ui-community/.env.e2e", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", - "$TURBO_ROOT$/scripts/local-dev/**", + "$TURBO_ROOT$/packages/cellix/local-dev/src/**", "cucumber.js", "package.json" ], @@ -27,10 +26,9 @@ "src/**/*.ts", "config/**", "$TURBO_ROOT$/apps/api/local-settings.e2e.json", - "$TURBO_ROOT$/apps/api/scripts/sync-local-settings.mjs", "$TURBO_ROOT$/apps/ui-community/.env.e2e", "$TURBO_ROOT$/apps/ui-staff/.env.e2e", - "$TURBO_ROOT$/scripts/local-dev/**", + "$TURBO_ROOT$/packages/cellix/local-dev/src/**", "cucumber.js", "package.json" ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e935c90aa..d8fd2dd2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,12 +299,15 @@ importers: '@cellix/config-vitest': specifier: workspace:* version: link:../../packages/cellix/config-vitest + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(vitest@4.1.6) azurite: specifier: ^3.35.0 - version: 3.35.0(@azure/core-client@1.10.1)(@types/node@22.19.15) + version: 3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -316,7 +319,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/docs: dependencies: @@ -351,6 +354,9 @@ importers: '@cellix/config-vitest': specifier: workspace:* version: link:../../packages/cellix/config-vitest + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev '@docusaurus/module-type-aliases': specifier: 3.9.2 version: 3.9.2(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -400,12 +406,12 @@ importers: '@cellix/config-typescript': specifier: workspace:* version: link:../../packages/cellix/config-typescript + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev rimraf: specifier: 'catalog:' version: 6.0.1 - tsx: - specifier: 'catalog:' - version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.3 @@ -425,6 +431,9 @@ importers: '@cellix/config-vitest': specifier: workspace:* version: link:../../packages/cellix/config-vitest + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(vitest@4.1.6) @@ -495,6 +504,9 @@ importers: '@cellix/config-vitest': specifier: workspace:* version: link:../../packages/cellix/config-vitest + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev '@chromatic-com/storybook': specifier: ^4.1.1 version: 4.1.3(storybook@9.1.20(@testing-library/dom@10.4.1)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))) @@ -625,6 +637,9 @@ importers: '@cellix/config-vitest': specifier: workspace:* version: link:../../packages/cellix/config-vitest + '@cellix/local-dev': + specifier: workspace:* + version: link:../../packages/cellix/local-dev '@types/react': specifier: ^19.1.8 version: 19.2.7 @@ -859,6 +874,30 @@ importers: specifier: 'catalog:' version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/cellix/local-dev: + devDependencies: + '@cellix/config-typescript': + specifier: workspace:* + version: link:../config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../config-vitest + '@types/node': + specifier: 'catalog:' + version: 22.19.15 + '@vitest/coverage-istanbul': + specifier: 'catalog:' + version: 4.1.6(vitest@4.1.6) + rimraf: + specifier: 'catalog:' + version: 6.0.1 + typescript: + specifier: 'catalog:' + version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/cellix/mongoose-seedwork: dependencies: '@cellix/domain-seedwork': @@ -19760,7 +19799,7 @@ snapshots: transitivePeerDependencies: - debug - azurite@3.35.0(@azure/core-client@1.10.1)(@types/node@22.19.15): + azurite@3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1): dependencies: '@azure/ms-rest-js': 1.11.2 applicationinsights: 2.9.8 @@ -19774,9 +19813,9 @@ snapshots: lokijs: 1.5.12 morgan: 1.10.1 multistream: 2.1.1 - mysql2: 3.22.3(@types/node@22.19.15) + mysql2: 3.22.3(@types/node@24.10.1) rimraf: 3.0.2 - sequelize: 6.37.8(mysql2@3.22.3(@types/node@22.19.15))(tedious@16.7.1(@azure/core-client@1.10.1)) + sequelize: 6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)) stoppable: 1.1.0 tedious: 16.7.1(@azure/core-client@1.10.1) to-readable-stream: 2.1.0 @@ -23662,9 +23701,9 @@ snapshots: mute-stream@0.0.8: {} - mysql2@3.22.3(@types/node@22.19.15): + mysql2@3.22.3(@types/node@24.10.1): dependencies: - '@types/node': 22.19.15 + '@types/node': 24.10.1 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 @@ -25485,7 +25524,7 @@ snapshots: sequelize-pool@7.1.0: {} - sequelize@6.37.8(mysql2@3.22.3(@types/node@22.19.15))(tedious@16.7.1(@azure/core-client@1.10.1)): + sequelize@6.37.8(mysql2@3.22.3(@types/node@24.10.1))(tedious@16.7.1(@azure/core-client@1.10.1)): dependencies: '@types/debug': 4.1.12 '@types/validator': 13.15.10 @@ -25504,7 +25543,7 @@ snapshots: validator: 13.15.35 wkx: 0.5.0 optionalDependencies: - mysql2: 3.22.3(@types/node@22.19.15) + mysql2: 3.22.3(@types/node@24.10.1) tedious: 16.7.1(@azure/core-client@1.10.1) transitivePeerDependencies: - supports-color diff --git a/scripts/local-dev/dev-process-exit.mjs b/scripts/local-dev/dev-process-exit.mjs deleted file mode 100644 index ed0232915..000000000 --- a/scripts/local-dev/dev-process-exit.mjs +++ /dev/null @@ -1,25 +0,0 @@ -/** @typedef {import('node:child_process').ChildProcess} ChildProcess */ - -/** - * @param {NodeJS.Signals | null | undefined} signal - * @param {number | null | undefined} code - * @returns {boolean} - */ -export const isGracefulInterruptExit = (signal, code) => signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT' || code === 130 || code === 143; - -/** - * Wires a spawned dev child process to forward its exit status to the parent, - * treating Turbo's interrupt signals as graceful exits. Every `start-dev.mjs` - * runner ends with the same handler, so this is the single source of truth. - * @param {ChildProcess} child - * @returns {void} - */ -export function forwardChildExit(child) { - child.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; - return; - } - process.exitCode = code ?? 1; - }); -} diff --git a/scripts/local-dev/portless-hostnames.mjs b/scripts/local-dev/portless-hostnames.mjs deleted file mode 100644 index 976c82e92..000000000 --- a/scripts/local-dev/portless-hostnames.mjs +++ /dev/null @@ -1,97 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -/** - * @typedef {Record} DotEnvValues - * @typedef {object} PortlessHostnames - * @property {string} uiCommunity - * @property {string} uiStaff - * @property {string} api - * @property {string} mockAuth - * @property {string} docs - */ - -const PORTLESS_PORT = 1355; -const scriptDir = fileURLToPath(new URL('.', import.meta.url)); -const workspaceRoot = resolve(scriptDir, '../..'); - -/** - * @param {string} filePath - * @returns {DotEnvValues} - */ -function readDotEnv(filePath) { - if (!existsSync(filePath)) return {}; - /** @type {DotEnvValues} */ - const result = {}; - for (const line of readFileSync(filePath, 'utf-8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - const eqIdx = trimmed.indexOf('='); - if (eqIdx === -1) continue; - result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); - } - return result; -} - -/** - * @param {string} url - * @returns {string | null} - */ -function hostnameFrom(url) { - try { - return new URL(url).hostname; - } catch { - return null; - } -} - -/** - * @param {string} key - * @param {DotEnvValues} values - * @returns {string | null} - */ -function hostnameFor(key, values) { - return hostnameFrom(process.env[key] ?? values[key] ?? ''); -} - -function applyWorktreeSuffix(hostname, worktreeName) { - if (!worktreeName) return hostname; - return hostname.replace('.localhost', `.${worktreeName}.localhost`); -} - -export function getHostnames() { - const uiEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-community/.env')); - const staffEnv = readDotEnv(resolve(workspaceRoot, 'apps/ui-staff/.env')); - const wt = process.env.WORKTREE_NAME ?? ''; - - const uiCommunity = hostnameFor('VITE_APP_UI_COMMUNITY_BASE_URL', uiEnv); - const api = hostnameFor('VITE_COMMON_API_ENDPOINT', uiEnv); - const mockAuth = hostnameFor('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiEnv); - const uiStaff = hostnameFor('VITE_APP_UI_STAFF_AAD_REDIRECT_URI', staffEnv); - - if (!uiCommunity || !api || !mockAuth || !uiStaff) { - throw new Error('portless-hostnames: could not derive all hostnames from .env files. ' + 'Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); - } - const docs = `docs.${uiCommunity}`; - - return { - uiCommunity: applyWorktreeSuffix(uiCommunity, wt), - uiStaff: applyWorktreeSuffix(uiStaff, wt), - api: applyWorktreeSuffix(api, wt), - mockAuth: applyWorktreeSuffix(mockAuth, wt), - docs: applyWorktreeSuffix(docs, wt), - }; -} - -/** - * Builds a full portless-proxied URL for the given hostname and optional path. - * @param {string} hostname - * @param {string} [path] - * @returns {string} - */ -export function buildPortlessUrl(hostname, path = '') { - return `https://${hostname}:${PORTLESS_PORT}${path}`; -} - -export { PORTLESS_PORT }; diff --git a/scripts/local-dev/vite-dev-args.mjs b/scripts/local-dev/vite-dev-args.mjs deleted file mode 100644 index dcc39bc77..000000000 --- a/scripts/local-dev/vite-dev-args.mjs +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @param {NodeJS.ProcessEnv} [env] - * @returns {boolean} - */ -export function isE2E(env = process.env) { - return ['1', 'true', 'yes'].includes((env.E2E ?? '').toLowerCase()); -} - -/** - * @param {{ host?: string; port?: string; env?: NodeJS.ProcessEnv }} [options] - * @returns {string[]} - */ -export function buildViteArgs(options = {}) { - const { host = '127.0.0.1', port, env = process.env } = options; - const args = ['--host', host]; - if (port) { - args.push('--port', port); - } - - const viteMode = env.E2E_VITE_MODE ?? (isE2E(env) || env.TF_BUILD ? 'e2e' : undefined); - if (viteMode) { - args.push('--mode', viteMode); - } - - return args; -} diff --git a/scripts/local-dev/worktree-ports.mjs b/scripts/local-dev/worktree-ports.mjs deleted file mode 100644 index de11700d6..000000000 --- a/scripts/local-dev/worktree-ports.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -/** - * @typedef {Record} SettingsValues - * @typedef {{ blob: number, queue: number, table: number }} AzuritePorts - */ - -/** - * Worktree-scoped port computation for service isolation. - * - * When WORKTREE_NAME is set, each worktree gets a deterministic port offset - * so MongoDB and Azurite instances don't collide between worktrees. - * - * Default worktree (no WORKTREE_NAME): uses base ports (50000, 10000–10002). - * Named worktree: base + deterministic offset derived from the name's hash. - * - * Collision safety: the unset case always returns 0, and any named worktree - * always returns ≥ 100, so the default worktree can never collide with a - * named one. With 49 buckets the chance of two *named* worktrees colliding - * is ~2% per pair — acceptable for the typical 1–3 concurrent worktrees. - */ - -const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); -const apiLocalSettingsPaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; -/** @type {SettingsValues | undefined} */ -let apiLocalSettingsValues; - -/** - * @param {string} name - * @param {SettingsValues} [values] - * @returns {string | undefined} - */ -function getSetting(name, values) { - return process.env[name] ?? values?.[name] ?? getApiLocalSetting(name); -} - -/** - * @param {string} name - * @returns {string | undefined} - */ -function getApiLocalSetting(name) { - apiLocalSettingsValues ??= readApiLocalSettingsValues(); - return apiLocalSettingsValues[name]; -} - -/** - * @returns {SettingsValues} - */ -function readApiLocalSettingsValues() { - for (const settingsPath of apiLocalSettingsPaths) { - if (!fs.existsSync(settingsPath)) continue; - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - return settings.Values ?? {}; - } - return {}; -} - -/** - * Returns a deterministic port offset in the range [100, 4900] (step 100) - * for the current worktree. Returns 0 when WORKTREE_NAME is not set. - * @returns {number} - */ -export function getWorktreePortOffset() { - const name = process.env.WORKTREE_NAME; - if (!name) return 0; - let hash = 0; - for (const c of name) hash = ((hash << 5) - hash + c.charCodeAt(0)) | 0; - return ((Math.abs(hash) % 49) + 1) * 100; -} - -/** - * MongoDB port for the current worktree. - * @returns {number} - */ -export function getMongoPort() { - return 50000 + getWorktreePortOffset(); -} - -/** - * Azurite blob/queue/table ports for the current worktree. - * @returns {AzuritePorts} - */ -export function getAzuritePorts() { - const offset = getWorktreePortOffset(); - return { - blob: 10000 + offset, - queue: 10001 + offset, - table: 10002 + offset, - }; -} - -/** - * Azurite connection string for worktree-specific ports. - * Returns `UseDevelopmentStorage=true` for the default worktree (port 10000). - * @param {SettingsValues} [values] - * @returns {string} - */ -export function getAzuriteConnectionString(values) { - const ports = getAzuritePorts(); - if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; - const accountName = getSetting('STORAGE_ACCOUNT_NAME', values); - const accountKey = getSetting('STORAGE_ACCOUNT_KEY', values); - if (!accountName || !accountKey) { - throw new Error('[worktree-ports] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); - } - return [ - 'DefaultEndpointsProtocol=http', - `AccountName=${accountName}`, - `AccountKey=${accountKey}`, - `BlobEndpoint=http://127.0.0.1:${ports.blob}/${accountName}`, - `QueueEndpoint=http://127.0.0.1:${ports.queue}/${accountName}`, - `TableEndpoint=http://127.0.0.1:${ports.table}/${accountName}`, - ].join(';'); -} - -/** - * MongoDB connection string with the worktree-specific port patched in. - * Reads COSMOSDB_CONNECTION_STRING from env or local.settings.json and replaces - * the host:port segment. - * @returns {string} - */ -export function getMongoConnectionString() { - const base = getSetting('COSMOSDB_CONNECTION_STRING'); - if (!base) throw new Error('[worktree-ports] COSMOSDB_CONNECTION_STRING must be set'); - const url = new URL(base); - url.port = String(getMongoPort()); - return url.toString(); -} diff --git a/sonar-project.properties b/sonar-project.properties index 0cffddb63..e5b7d641a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -95,7 +95,7 @@ sonar.test.inclusions=**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.spec.tsx,**/* sonar.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/** # Coverage exclusions -sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,scripts/local-dev/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx +sonar.coverage.exclusions=**/*.config.ts,**/tsconfig.json,**/.storybook/**,**/*.stories.ts,**/*.stories.tsx,**/*.test.ts,**/*.test.tsx,**/*.generated.ts,**/*.generated.tsx,**/*.d.ts,**/dist/**,**/deploy/**,**/coverage/**,apps/docs/src/test/**,build-pipeline/scripts/**,packages/ocom/domain/tests/**,packages/cellix/server-oauth2-mock-seedwork/**,packages/cellix/server-mongodb-memory-mock-seedwork/**,packages/ocom/data-sources-mongoose-models/**,packages/ocom/graphql/src/schema/builder/schema-builder.ts,apps/api/src/index.ts,apps/api/src/service-config/**,packages/cellix/archunit-tests/**,packages/ocom-verification/archunit-tests/**,packages/cellix/ui-core/**,apps/ui-community/**,packages/ocom/ui-shared/src/components/organisms/header/index.tsx # CPD (code duplication) exclusions sonar.cpd.exclusions=**/*.test.ts,**/*.generated.ts,**/*.generated.tsx,packages/cellix/archunit-tests/src/test-suites/**,packages/cellix/archunit-tests/src/fixtures/** From 0f53ffce70d2ab0a0b1ed7b5958cb8aad1a7a64a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 5 Jun 2026 16:02:10 -0400 Subject: [PATCH 21/40] progress - cleaned up cellix package and created ocom package for local app specific settings --- .pnpm-store/v11/index.db | Bin 0 -> 8192 bytes apps/api/package.json | 9 +- apps/api/start-azurite.ts | 16 ++ apps/api/start-dev.ts | 3 + apps/api/sync-local-settings.ts | 64 ++++++ apps/docs/package.json | 4 +- apps/docs/start-dev.ts | 3 + apps/server-mongodb-memory-mock/package.json | 4 +- apps/server-mongodb-memory-mock/start-dev.ts | 8 + apps/server-oauth2-mock/package.json | 5 +- apps/server-oauth2-mock/start-dev.ts | 14 ++ apps/ui-community/package.json | 5 +- apps/ui-community/start-dev.ts | 15 ++ apps/ui-staff/package.json | 7 +- apps/ui-staff/start-dev.ts | 15 ++ knip.json | 12 +- packages/cellix/local-dev/README.md | 109 +++++----- .../cellix/local-dev/bin/cellix-local-dev.mjs | 22 --- packages/cellix/local-dev/manifest.md | 45 ++--- packages/cellix/local-dev/package.json | 6 +- .../local-dev/src/api-local-settings.ts | 85 -------- packages/cellix/local-dev/src/bin.ts | 7 - packages/cellix/local-dev/src/cli.ts | 121 ------------ packages/cellix/local-dev/src/dotenv.ts | 31 +++ packages/cellix/local-dev/src/hostnames.ts | 91 --------- packages/cellix/local-dev/src/index.test.ts | 141 +++++++------ packages/cellix/local-dev/src/index.ts | 33 ++-- packages/cellix/local-dev/src/json-files.ts | 42 ++++ packages/cellix/local-dev/src/runners.ts | 187 ++++++------------ packages/cellix/local-dev/src/urls.ts | 40 ++++ packages/cellix/local-dev/src/vite.ts | 12 +- .../cellix/local-dev/src/worktree-ports.ts | 78 ++------ .../e2e-tests/src/cucumber-lifecycle-hooks.ts | 1 + .../e2e-tests/src/infrastructure.ts | 4 +- .../shared/environment/test-environment.ts | 14 ++ packages/ocom/local-dev-config/.gitignore | 2 + packages/ocom/local-dev-config/package.json | 47 +++++ .../ocom/local-dev-config/src/hostnames.ts | 51 +++++ packages/ocom/local-dev-config/src/index.ts | 4 + packages/ocom/local-dev-config/src/types.ts | 25 +++ packages/ocom/local-dev-config/src/urls.ts | 22 +++ .../ocom/local-dev-config/src/workspace.ts | 5 + packages/ocom/local-dev-config/tsconfig.json | 10 + pnpm-lock.yaml | 31 +++ turbo.json | 7 +- 45 files changed, 747 insertions(+), 710 deletions(-) create mode 100644 .pnpm-store/v11/index.db create mode 100644 apps/api/start-azurite.ts create mode 100644 apps/api/start-dev.ts create mode 100644 apps/api/sync-local-settings.ts create mode 100644 apps/docs/start-dev.ts create mode 100644 apps/server-mongodb-memory-mock/start-dev.ts create mode 100644 apps/server-oauth2-mock/start-dev.ts create mode 100644 apps/ui-community/start-dev.ts create mode 100644 apps/ui-staff/start-dev.ts delete mode 100755 packages/cellix/local-dev/bin/cellix-local-dev.mjs delete mode 100644 packages/cellix/local-dev/src/api-local-settings.ts delete mode 100644 packages/cellix/local-dev/src/bin.ts delete mode 100644 packages/cellix/local-dev/src/cli.ts create mode 100644 packages/cellix/local-dev/src/dotenv.ts delete mode 100644 packages/cellix/local-dev/src/hostnames.ts create mode 100644 packages/cellix/local-dev/src/json-files.ts create mode 100644 packages/cellix/local-dev/src/urls.ts create mode 100644 packages/ocom/local-dev-config/.gitignore create mode 100644 packages/ocom/local-dev-config/package.json create mode 100644 packages/ocom/local-dev-config/src/hostnames.ts create mode 100644 packages/ocom/local-dev-config/src/index.ts create mode 100644 packages/ocom/local-dev-config/src/types.ts create mode 100644 packages/ocom/local-dev-config/src/urls.ts create mode 100644 packages/ocom/local-dev-config/src/workspace.ts create mode 100644 packages/ocom/local-dev-config/tsconfig.json diff --git a/.pnpm-store/v11/index.db b/.pnpm-store/v11/index.db new file mode 100644 index 0000000000000000000000000000000000000000..044636a65ff67410fad601f0c39978108fefcea0 GIT binary patch literal 8192 zcmeIuzpBD86bA4#2p0s=&GDX11#$5OY&BppTCFMSqD0LV@%|C%po4=C;0rt5R!Xsx zd-*<+oFpepe$$EEhlalXPCq)NHmfksS%-)*#*-P9XRK%~B>T9;=Xc?(b$^tiS5|q+ zqJcmF0uX=z1Rwwb2tWV=5P$##awu^7v_7h}nsvK|di`yVdUMb_v)cb|%{g=6U0>Kr zkg^>qDAS^Pkk?+mi kUUFZI)hjuq$Cn@g0SG_<0uX=z1Rwwb2tWV=5J(070rxR8{{R30 literal 0 HcmV?d00001 diff --git a/apps/api/package.json b/apps/api/package.json index 330501df0..ec7246dd2 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,9 +9,9 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev": "pnpm exec portless data-access.ownercommunity.localhost --force cellix-local-dev azure-functions", + "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.ts", "predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev azure-functions", + "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", @@ -23,8 +23,8 @@ "clean": "rimraf dist deploy", "prestart": "pnpm run prepare:deploy && pnpm run sync-local-settings", "start": "func start --typescript --script-root deploy/", - "sync-local-settings": "cellix-local-dev sync-api-local-settings", - "azurite": "cellix-local-dev azurite" + "sync-local-settings": "node sync-local-settings.ts", + "azurite": "node start-azurite.ts" }, "dependencies": { "@azure/functions": "catalog:", @@ -49,6 +49,7 @@ "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@cellix/local-dev": "workspace:*", + "@ocom/local-dev-config": "workspace:*", "@vitest/coverage-istanbul": "catalog:", "azurite": "^3.35.0", "rimraf": "catalog:", diff --git a/apps/api/start-azurite.ts b/apps/api/start-azurite.ts new file mode 100644 index 000000000..a95930199 --- /dev/null +++ b/apps/api/start-azurite.ts @@ -0,0 +1,16 @@ +import path from 'node:path'; +import { getAzuritePorts, resolveWorkspaceRoot, runAzuriteDev } from '@cellix/local-dev'; + +const workspaceRoot = resolveWorkspaceRoot(); +const worktreeName = process.env['WORKTREE_NAME']; +const ports = getAzuritePorts(worktreeName); +const storageSuffix = worktreeName ? `-${worktreeName}` : ''; + +runAzuriteDev({ + blobPort: ports.blob, + blobLocation: path.join(workspaceRoot, `__blobstorage__${storageSuffix}`), + queuePort: ports.queue, + queueLocation: path.join(workspaceRoot, `__queuestorage__${storageSuffix}`), + tablePort: ports.table, + tableLocation: path.join(workspaceRoot, `__tablestorage__${storageSuffix}`), +}); diff --git a/apps/api/start-dev.ts b/apps/api/start-dev.ts new file mode 100644 index 000000000..f637f6bc0 --- /dev/null +++ b/apps/api/start-dev.ts @@ -0,0 +1,3 @@ +import { runAzureFunctionsDev } from '@cellix/local-dev'; + +runAzureFunctionsDev(); diff --git a/apps/api/sync-local-settings.ts b/apps/api/sync-local-settings.ts new file mode 100644 index 000000000..6998ba94c --- /dev/null +++ b/apps/api/sync-local-settings.ts @@ -0,0 +1,64 @@ +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { buildAzuriteConnectionString, getAzuritePorts, getMongoPort, replaceUrlPort, resolveWorkspaceRoot, syncJsonFile } from '@cellix/local-dev'; +import { buildOcomUrls } from '@ocom/local-dev-config'; + +const workspaceRoot = resolveWorkspaceRoot(); +const appDir = process.cwd(); +const env = process.env; +const isE2E = ['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()); +const localSettingsPath = path.join(appDir, 'local.settings.json'); +const e2eLocalSettingsPath = path.join(appDir, 'local-settings.e2e.json'); +const targetPath = path.join(appDir, 'deploy', 'local.settings.json'); + +if (!isE2E) { + if (!existsSync(localSettingsPath)) { + process.exit(0); + } + + syncJsonFile({ + sourcePath: localSettingsPath, + targetPath, + }); + process.exit(0); +} + +const urls = buildOcomUrls({ env, workspaceRoot }); +const worktreeName = env['WORKTREE_NAME']; +const ports = getAzuritePorts(worktreeName); + +syncJsonFile({ + sourcePath: e2eLocalSettingsPath, + targetPath, + transform: (document) => { + const settings = document; + const values = { ...(settings.Values ?? {}) }; + const accountName = String(values['STORAGE_ACCOUNT_NAME'] ?? ''); + const accountKey = String(values['STORAGE_ACCOUNT_KEY'] ?? ''); + + values['ACCOUNT_PORTAL_OIDC_ISSUER'] = urls.mockCommunityAuthorityUrl; + values['ACCOUNT_PORTAL_OIDC_ENDPOINT'] = urls.mockCommunityJwksUrl; + values['STAFF_PORTAL_OIDC_ISSUER'] = urls.mockStaffAuthorityUrl; + values['STAFF_PORTAL_OIDC_ENDPOINT'] = urls.mockStaffJwksUrl; + + if (accountName && accountKey && worktreeName) { + const azuriteConnectionString = buildAzuriteConnectionString({ + accountKey, + accountName, + ports, + }); + values['AZURE_STORAGE_CONNECTION_STRING'] = azuriteConnectionString; + values['AzureWebJobsStorage'] = azuriteConnectionString; + } + + const cosmosConnectionString = env['COSMOSDB_CONNECTION_STRING'] ?? values['COSMOSDB_CONNECTION_STRING']; + if (typeof cosmosConnectionString === 'string' && cosmosConnectionString) { + values['COSMOSDB_CONNECTION_STRING'] = worktreeName ? replaceUrlPort(cosmosConnectionString, getMongoPort(worktreeName)) : cosmosConnectionString; + } + + return { + ...settings, + Values: values, + }; + }, +}); diff --git a/apps/docs/package.json b/apps/docs/package.json index a8a526048..d810004d4 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "dev": "pnpm exec portless docs.ownercommunity.localhost --force cellix-local-dev docusaurus", - "dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev docusaurus", + "dev": "pnpm exec portless docs.ownercommunity.localhost --force node start-dev.ts", + "dev:worktree": "pnpm exec portless docs.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "start": "docusaurus start --port 3001", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/apps/docs/start-dev.ts b/apps/docs/start-dev.ts new file mode 100644 index 000000000..7b8c1655c --- /dev/null +++ b/apps/docs/start-dev.ts @@ -0,0 +1,3 @@ +import { runDocusaurusDev } from '@cellix/local-dev'; + +runDocusaurusDev(); diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index 96109e079..1a5acc021 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -11,8 +11,8 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "cellix-local-dev tsx --profile mongo-memory-mock", - "dev:worktree": "cellix-local-dev tsx --profile mongo-memory-mock" + "dev": "node start-dev.ts", + "dev:worktree": "node start-dev.ts" }, "dependencies": { "@cellix/server-mongodb-memory-mock-seedwork": "workspace:*", diff --git a/apps/server-mongodb-memory-mock/start-dev.ts b/apps/server-mongodb-memory-mock/start-dev.ts new file mode 100644 index 000000000..a1afd8bd2 --- /dev/null +++ b/apps/server-mongodb-memory-mock/start-dev.ts @@ -0,0 +1,8 @@ +import { getMongoPort, runTsxDev } from '@cellix/local-dev'; + +runTsxDev({ + env: { + ...process.env, + PORT: String(getMongoPort(process.env['WORKTREE_NAME'])), + }, +}); diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index 6c0c8ae07..28e3fadc3 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -11,14 +11,15 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force cellix-local-dev tsx --profile oauth2-mock", - "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev tsx --profile oauth2-mock", + "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force node start-dev.ts", + "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest" }, "dependencies": { "@cellix/server-oauth2-mock-seedwork": "workspace:*", + "@ocom/local-dev-config": "workspace:*", "dotenv": "^16.4.5" }, "devDependencies": { diff --git a/apps/server-oauth2-mock/start-dev.ts b/apps/server-oauth2-mock/start-dev.ts new file mode 100644 index 000000000..d26144b72 --- /dev/null +++ b/apps/server-oauth2-mock/start-dev.ts @@ -0,0 +1,14 @@ +/// +import { runTsxDev } from '@cellix/local-dev'; +import { buildOcomUrls } from '@ocom/local-dev-config'; + +const urls = buildOcomUrls(); + +runTsxDev({ + env: { + ...process.env, + BASE_URL: urls.mockCommunityAuthorityUrl.replace('/community', ''), + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: urls.uiCommunityRedirectUrl, + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: urls.uiStaffRedirectUrl, + }, +}); diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index a4391587e..51b4896d4 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -9,8 +9,8 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless ownercommunity.localhost --force cellix-local-dev vite --profile ui-community", - "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-community", + "dev": "pnpm exec portless ownercommunity.localhost --force node start-dev.ts", + "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", @@ -24,6 +24,7 @@ "@cellix/ui-core": "workspace:*", "@dr.pogodin/react-helmet": "^3.0.2", "@graphql-typed-document-node/core": "^3.2.0", + "@ocom/local-dev-config": "workspace:*", "@ocom/ui-community-route-accounts": "workspace:*", "@ocom/ui-community-route-admin": "workspace:*", "@ocom/ui-community-route-root": "workspace:*", diff --git a/apps/ui-community/start-dev.ts b/apps/ui-community/start-dev.ts new file mode 100644 index 000000000..47cf4d758 --- /dev/null +++ b/apps/ui-community/start-dev.ts @@ -0,0 +1,15 @@ +/// +import { runViteDev } from '@cellix/local-dev'; +import { buildOcomUrls } from '@ocom/local-dev-config'; + +const urls = buildOcomUrls(); + +runViteDev({ + env: { + ...process.env, + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: urls.mockCommunityAuthorityUrl, + VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: urls.uiCommunityRedirectUrl, + VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, + VITE_APP_UI_COMMUNITY_BASE_URL: urls.uiCommunityBaseUrl, + }, +}); diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index 70158d368..e270f814a 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -9,8 +9,8 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless staff.ownercommunity.localhost --force cellix-local-dev vite --profile ui-staff", - "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-staff", + "dev": "pnpm exec portless staff.ownercommunity.localhost --force node start-dev.ts", + "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", @@ -21,8 +21,9 @@ "@apollo/client": "^3.13.9", "@dr.pogodin/react-helmet": "^3.0.2", "@cellix/ui-core": "workspace:*", - "@ocom/ui-shared": "workspace:*", "@graphql-typed-document-node/core": "^3.2.0", + "@ocom/local-dev-config": "workspace:*", + "@ocom/ui-shared": "workspace:*", "@ocom/ui-staff-route-root": "workspace:*", "@ocom/ui-staff-route-community-management": "workspace:*", "@ocom/ui-staff-route-user-management": "workspace:*", diff --git a/apps/ui-staff/start-dev.ts b/apps/ui-staff/start-dev.ts new file mode 100644 index 000000000..f77a3550b --- /dev/null +++ b/apps/ui-staff/start-dev.ts @@ -0,0 +1,15 @@ +/// +import { runViteDev } from '@cellix/local-dev'; +import { buildOcomUrls } from '@ocom/local-dev-config'; + +const urls = buildOcomUrls(); + +runViteDev({ + env: { + ...process.env, + VITE_APP_UI_STAFF_AAD_AUTHORITY: urls.mockStaffAuthorityUrl, + VITE_APP_UI_STAFF_AAD_REDIRECT_URI: urls.uiStaffRedirectUrl, + VITE_APP_UI_STAFF_BASE_URL: urls.uiStaffBaseUrl, + VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, + }, +}); diff --git a/knip.json b/knip.json index fa91a2bc3..eaff37f94 100644 --- a/knip.json +++ b/knip.json @@ -7,14 +7,14 @@ "ignoreDependencies": ["azurite"] }, "apps/ui-community": { - "entry": ["src/main.tsx"], - "project": ["src/**/*.{ts,tsx}"], + "entry": ["src/main.tsx", "start-dev.ts"], + "project": ["src/**/*.{ts,tsx}", "start-dev.ts"], "ignore": ["**/apollo-client-links.tsx"], "ignoreDependencies": ["@cellix/local-dev"] }, "apps/ui-staff": { - "entry": ["src/main.tsx"], - "project": ["src/**/*.{ts,tsx}"], + "entry": ["src/main.tsx", "start-dev.ts"], + "project": ["src/**/*.{ts,tsx}", "start-dev.ts"], "ignore": [], "ignoreDependencies": ["@cellix/local-dev"] }, @@ -91,8 +91,8 @@ "ignoreUnresolved": ["progress-bar"] }, "apps/server-oauth2-mock": { - "entry": ["src/index.ts"], - "project": ["src/**/*.ts"], + "entry": ["src/index.ts", "start-dev.ts"], + "project": ["src/**/*.ts", "start-dev.ts"], "ignoreDependencies": ["@cellix/local-dev", "tsx"] }, "apps/server-mongodb-memory-mock": { diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 84dd87026..3fe57fdab 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -1,20 +1,8 @@ # @cellix/local-dev -Shared local-development runtime for Cellix application packages. +Generic local-development helpers for Cellix app wrappers. -This package replaces duplicated `start-dev.*`, `start-mongo.*`, `start-azurite.*`, and root `scripts/local-dev/*` orchestration with one reusable package plus a small CLI. - -## What this package provides - -- Worktree-aware portless hostname derivation -- Worktree-aware MongoDB and Azurite port derivation -- API local-settings sync for normal and `e2e` modes -- Shared dev runners for: - - Vite apps - - Docusaurus docs - - Azure Functions local startup - - TSX-backed mock servers - - Azurite +This package is intentionally policy-free. It owns reusable mechanics such as worktree port math, URL helpers, JSON and dotenv utilities, process exit forwarding, and generic dev runners. App-specific env keys, hostnames, auth routes, and `local.settings.json` mutations belong in app-owned wrapper scripts or internal repo helpers, not here. ## Install @@ -28,55 +16,74 @@ In this monorepo, app packages consume the workspace package directly: } ``` -## CLI usage - -```bash -cellix-local-dev vite --profile ui-community -cellix-local-dev vite --profile ui-staff -cellix-local-dev docusaurus -cellix-local-dev azure-functions -cellix-local-dev tsx --profile oauth2-mock -cellix-local-dev tsx --profile mongo-memory-mock -cellix-local-dev azurite -cellix-local-dev sync-api-local-settings -cellix-local-dev sync-api-local-settings e2e -``` +## What this package provides -## Example app scripts +- Workspace-root discovery +- Dotenv parsing and JSON file sync helpers +- Worktree-aware hostname and URL utilities +- Worktree-aware MongoDB and Azurite port derivation +- Azurite connection-string construction from explicit credentials +- Generic dev runners for: + - Vite + - Docusaurus + - Azure Functions + - TSX-backed processes + - Azurite -```json -{ - "scripts": { - "dev": "pnpm exec portless ownercommunity.localhost --force cellix-local-dev vite --profile ui-community", - "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force cellix-local-dev vite --profile ui-community" - } -} +## Recommended consumption pattern + +Keep the app policy in a wrapper script and compose this package's generic helpers inside it: + +```js +import { buildPortlessUrl, getMongoPort, runTsxDev } from '@cellix/local-dev'; + +runTsxDev({ + env: { + ...process.env, + BASE_URL: buildPortlessUrl('mock-auth.example.localhost'), + PORT: String(getMongoPort(process.env.WORKTREE_NAME)), + }, +}); ``` -```json -{ - "scripts": { - "sync-local-settings": "cellix-local-dev sync-api-local-settings", - "dev": "pnpm exec portless data-access.ownercommunity.localhost --force cellix-local-dev azure-functions", - "azurite": "cellix-local-dev azurite" - } -} +For settings files, let the app decide what to change: + +```js +import { syncJsonFile } from '@cellix/local-dev'; + +syncJsonFile({ + sourcePath: 'local-settings.e2e.json', + targetPath: 'deploy/local.settings.json', + transform: (document) => ({ + ...document, + Values: { + ...(document.Values ?? {}), + MODE: 'e2e', + }, + }), +}); ``` ## Public API -The package also exports the local-dev primitives for tests or scripted composition: - - `resolveWorkspaceRoot` -- `resolvePortlessHostnames` +- `readDotEnv` +- `readJsonFile` +- `writeJsonFile` +- `syncJsonFile` +- `hostnameFromUrl` +- `applyWorktreeSuffix` - `buildPortlessUrl` +- `replaceUrlPort` +- `PORTLESS_PORT` - `buildViteArgs` +- `isE2E` +- `forwardChildExit` +- `isGracefulInterruptExit` - `getWorktreePortOffset` - `getMongoPort` - `getAzuritePorts` -- `getAzuriteConnectionString` -- `getMongoConnectionString` -- `syncApiLocalSettings` +- `buildAzuriteConnectionString` - `runViteDev` - `runDocusaurusDev` - `runAzureFunctionsDev` @@ -85,5 +92,5 @@ The package also exports the local-dev primitives for tests or scripted composit ## Notes -- The implementation is in TypeScript, but the package exposes a normal Node bin so consuming app scripts do not need to boot shared `.ts` files through `tsx`. -- The package derives the workspace root from the caller's current working directory, so app packages do not need to hardcode repo-relative paths. +- The package derives workspace roots from the caller's current working directory, but it does not infer app layouts or env-variable names. +- If a helper only exists to support one app's local policy, it should usually live with that app instead of being exported here. diff --git a/packages/cellix/local-dev/bin/cellix-local-dev.mjs b/packages/cellix/local-dev/bin/cellix-local-dev.mjs deleted file mode 100755 index ffe9cc635..000000000 --- a/packages/cellix/local-dev/bin/cellix-local-dev.mjs +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -import { spawn } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; - -const child = spawn(process.execPath, ['--experimental-strip-types', fileURLToPath(new URL('../src/bin.ts', import.meta.url)), ...process.argv.slice(2)], { - stdio: 'inherit', -}); - -child.on('error', (error) => { - console.error(`[local-dev] failed to start CLI: ${error.message}`); - process.exit(1); -}); - -child.on('exit', (code, signal) => { - if (signal) { - process.kill(process.pid, signal); - return; - } - - process.exit(code ?? 1); -}); diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index b6883617f..d577a0634 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -2,59 +2,60 @@ ## Purpose -Provide a single local-development runtime for Cellix app packages so app-level `dev`, `dev:worktree`, `azurite`, and local-settings scripts can stay configuration-shaped while the real orchestration lives in one reusable package. +Provide generic local-development primitives for Cellix app packages so app-owned wrapper scripts can supply project-specific env keys, hostnames, and local-settings policy without hardcoding those details into the shared package. ## Scope -This package owns worktree-aware hostname and port derivation, portless URL building, API local-settings rewriting, child-process lifecycle handling, and the shared dev-runner orchestration for Vite, Docusaurus, Azure Functions, TSX-based mock servers, and Azurite. +This package owns generic worktree-aware port math, URL helpers, dotenv and JSON file utilities, child-process lifecycle handling, and generic dev-runner orchestration for Vite, Docusaurus, Azure Functions, TSX-based processes, and Azurite. ## Non-goals - Production runtime behavior -- Generic process management beyond Cellix local-development use cases -- Replacing app build/start scripts that are not part of the local-development workflow +- App-specific env-variable names, hostnames, auth-provider paths, or local-settings keys +- Discovering repo-specific app folders from inside the package +- Replacing app-owned wrapper scripts that express local project policy ## Public API shape - `resolveWorkspaceRoot(options?)` -- `resolvePortlessHostnames(options?)`, `buildPortlessUrl(hostname, path?)`, `PORTLESS_PORT` +- `readDotEnv(filePath)` +- `readJsonFile(filePath)`, `writeJsonFile(filePath, data)`, `syncJsonFile(options)` +- `hostnameFromUrl(url)`, `applyWorktreeSuffix(hostname, worktreeName)`, `buildPortlessUrl(hostname, path?)`, `replaceUrlPort(url, port)`, `PORTLESS_PORT` - `isE2E(env?)`, `buildViteArgs(options?)` - `isGracefulInterruptExit(signal, code)`, `forwardChildExit(child)` -- `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)` -- `getAzuriteConnectionString(options?)`, `getMongoConnectionString(options?)` -- `syncApiLocalSettings(options?)` -- `runViteDev(profile, options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runTsxDev(profile, options?)`, `runAzuriteDev(options?)` +- `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)`, `buildAzuriteConnectionString(options)` +- `runViteDev(options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runTsxDev(options?)`, `runAzuriteDev(options)` ## Core concepts -- App packages should express local dev behavior as small parameter choices such as profile names or entry paths, not as duplicated process wiring. -- Worktree isolation is deterministic and should keep hostnames, MongoDB ports, and Azurite ports aligned across all participating apps. -- The package is allowed to know Cellix app profiles such as `ui-community`, `ui-staff`, `oauth2-mock`, and `mongo-memory-mock`, because those profiles are the stable consumer contract that replaces ad hoc app wrapper scripts. +- App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and settings-file transforms. +- This package should expose only reusable mechanics that make those wrappers smaller and more consistent. +- Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, and hostname suffixing aligned across all participating apps. ## Package boundaries -- Keep CLI argument parsing, process spawning helpers, file-system helpers, and app profile definitions internal unless consumers outside this package need them directly. -- Do not leak app-relative path assumptions into consumers; the package must derive workspace paths from the caller's current working directory. -- Avoid widening the public surface with one-off helpers that only exist to support a single internal branch. +- Do not encode OCOM app names, env-variable names, or `local.settings.json` schemas in this package. +- Keep repo-specific hostname and env mapping logic in app-owned scripts or internal repo helpers outside this package. +- Avoid widening the public surface with one-off helpers that only exist to support a single app branch. ## Dependencies / relationships - Downstream consumers in this monorepo: `@apps/api`, `@apps/docs`, `@apps/ui-community`, `@apps/ui-staff`, `@apps/server-oauth2-mock`, `@apps/server-mongodb-memory-mock` -- Consumed from package scripts through the `cellix-local-dev` bin and from tests through the TypeScript API +- Consumed from app-owned wrapper scripts and from tests through the TypeScript API ## Testing strategy -- Prefer public-entrypoint tests for hostname derivation, worktree port derivation, Vite arg building, connection-string patching, and API local-settings rewriting. -- Avoid tests that reach into internal CLI parsing or helper modules when a public function already proves the observable behavior. +- Prefer public-entrypoint tests for dotenv parsing, URL helpers, worktree port derivation, Vite arg building, Azurite connection-string building, and generic JSON syncing. +- Avoid tests that prove repo-specific wrapper policy through this package's public contract. ## Documentation obligations -- Keep `README.md` focused on how app packages consume the package. +- Keep `README.md` focused on the generic helper surface and how app-owned wrappers compose it. - Keep TSDoc aligned on public exports that define package behavior. -- Update this manifest when app profiles, public exports, or scope boundaries change. +- Update this manifest when public exports or scope boundaries change. ## Release-readiness standards -- App packages should need only profile/entry configuration in `package.json` after consuming this package. +- App packages should be able to express their policy without modifying this package. - Package build and package tests must pass, plus affected app builds/tests as justified by the migration. -- Any new app profile should be added deliberately and documented as part of the contract. +- New helper exports must solve a reusable mechanical problem rather than expose app policy. diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json index 10c3d2454..a1ea2b63e 100644 --- a/packages/cellix/local-dev/package.json +++ b/packages/cellix/local-dev/package.json @@ -1,11 +1,10 @@ { "name": "@cellix/local-dev", "version": "1.0.0", - "description": "Shared local-development runner orchestration for Cellix applications", + "description": "Generic local-development helpers for Cellix app wrappers", "type": "module", "files": [ "dist", - "bin", "src" ], "main": "dist/index.js", @@ -16,9 +15,6 @@ "default": "./dist/index.js" } }, - "bin": { - "cellix-local-dev": "bin/cellix-local-dev.mjs" - }, "scripts": { "prebuild": "pnpm run lint", "build": "tsgo --build", diff --git a/packages/cellix/local-dev/src/api-local-settings.ts b/packages/cellix/local-dev/src/api-local-settings.ts deleted file mode 100644 index f91a2a82b..000000000 --- a/packages/cellix/local-dev/src/api-local-settings.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { buildPortlessUrl, resolvePortlessHostnames } from './hostnames.ts'; -import { getAzuriteConnectionString } from './worktree-ports.ts'; - -interface ApiLocalSettingsDocument { - Values?: Record; -} - -interface SyncApiLocalSettingsOptions { - appDir?: string; - workspaceRoot?: string; - env?: NodeJS.ProcessEnv; - mode?: 'e2e'; -} - -/** - * Applies the shared Cellix e2e and worktree overrides to an API - * `local.settings.json` document. - */ -export function applyApiLocalSettingsOverrides(settings: ApiLocalSettingsDocument, options: Pick = {}): ApiLocalSettingsDocument { - const env = options.env ?? process.env; - const values = { ...(settings.Values ?? {}) }; - - if (env['WORKTREE_NAME']) { - const hostnames = resolvePortlessHostnames({ - ...(options.workspaceRoot ? { startDir: options.workspaceRoot } : {}), - env, - }); - values['ACCOUNT_PORTAL_OIDC_ISSUER'] = buildPortlessUrl(hostnames.mockAuth, '/community'); - values['ACCOUNT_PORTAL_OIDC_ENDPOINT'] = buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'); - values['STAFF_PORTAL_OIDC_ISSUER'] = buildPortlessUrl(hostnames.mockAuth, '/staff'); - values['STAFF_PORTAL_OIDC_ENDPOINT'] = buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'); - const azurite = getAzuriteConnectionString({ - ...(options.workspaceRoot ? { startDir: options.workspaceRoot } : {}), - env, - values, - }); - values['AZURE_STORAGE_CONNECTION_STRING'] = azurite; - values['AzureWebJobsStorage'] = azurite; - } - - if (env['COSMOSDB_CONNECTION_STRING']) { - values['COSMOSDB_CONNECTION_STRING'] = env['COSMOSDB_CONNECTION_STRING']; - } - - settings.Values = values; - return settings; -} - -/** - * Syncs `apps/api/deploy/local.settings.json` from the local or e2e source file. - */ -export function syncApiLocalSettings(options: SyncApiLocalSettingsOptions = {}): void { - const env = options.env ?? process.env; - const appDir = path.resolve(options.appDir ?? process.cwd()); - const mode = options.mode ?? (['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()) ? 'e2e' : undefined); - const localSettingsPath = path.join(appDir, 'local.settings.json'); - const e2eLocalSettingsPath = path.join(appDir, 'local-settings.e2e.json'); - const targetPath = path.join(appDir, 'deploy', 'local.settings.json'); - - mkdirSync(path.dirname(targetPath), { recursive: true }); - - if (!mode) { - if (existsSync(localSettingsPath)) { - copyFileSync(localSettingsPath, targetPath); - } - return; - } - - if (mode !== 'e2e') { - throw new Error(`[local-dev] Invalid mode: expected one of e2e, received "${mode}"`); - } - - if (!existsSync(e2eLocalSettingsPath)) { - throw new Error(`[local-dev] Missing local settings for mode "e2e": ${e2eLocalSettingsPath}`); - } - - const settings = JSON.parse(readFileSync(e2eLocalSettingsPath, 'utf8')) as ApiLocalSettingsDocument; - applyApiLocalSettingsOverrides(settings, { - workspaceRoot: options.workspaceRoot ?? appDir, - env, - }); - writeFileSync(targetPath, `${JSON.stringify(settings, null, '\t')}\n`); -} diff --git a/packages/cellix/local-dev/src/bin.ts b/packages/cellix/local-dev/src/bin.ts deleted file mode 100644 index e6bbb3f23..000000000 --- a/packages/cellix/local-dev/src/bin.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { runCli } from './cli.ts'; - -const exitCode = runCli(process.argv.slice(2)); - -if (typeof exitCode === 'number' && exitCode !== 0) { - process.exit(exitCode); -} diff --git a/packages/cellix/local-dev/src/cli.ts b/packages/cellix/local-dev/src/cli.ts deleted file mode 100644 index 1d9e1da4c..000000000 --- a/packages/cellix/local-dev/src/cli.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { syncApiLocalSettings } from './api-local-settings.ts'; -import { runAzureFunctionsDev, runAzuriteDev, runDocusaurusDev, runTsxDev, runViteDev, type TsxDevProfile, type ViteDevProfile } from './runners.ts'; - -type CliCommand = 'vite' | 'docusaurus' | 'azure-functions' | 'tsx' | 'azurite' | 'sync-api-local-settings'; - -type FlagMap = Record; - -function parseFlags(args: string[]): { flags: FlagMap; positionals: string[] } { - const flags: FlagMap = {}; - const positionals: string[] = []; - - for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; - if (!arg) { - continue; - } - if (arg === '--help' || arg === '-h') { - flags['help'] = 'true'; - continue; - } - - if (arg.startsWith('--')) { - const key = arg.slice(2); - const value = args[index + 1]; - if (!value || value.startsWith('--')) { - throw new Error(`[local-dev] Missing value for --${key}`); - } - flags[key] = value; - index += 1; - continue; - } - - positionals.push(arg); - } - - return { flags, positionals }; -} - -function usage(): string { - return [ - 'Usage:', - ' cellix-local-dev vite --profile ', - ' cellix-local-dev docusaurus', - ' cellix-local-dev azure-functions', - ' cellix-local-dev tsx --profile [--entry src/index.ts]', - ' cellix-local-dev azurite', - ' cellix-local-dev sync-api-local-settings [e2e]', - ].join('\n'); -} - -function asViteProfile(value: string | undefined): ViteDevProfile { - if (value === 'ui-community' || value === 'ui-staff') { - return value; - } - throw new Error(`[local-dev] Invalid vite profile "${value ?? ''}"`); -} - -function asTsxProfile(value: string | undefined): TsxDevProfile { - if (value === 'oauth2-mock' || value === 'mongo-memory-mock') { - return value; - } - throw new Error(`[local-dev] Invalid tsx profile "${value ?? ''}"`); -} - -/** - * Runs the `cellix-local-dev` CLI. - */ -export function runCli(argv = process.argv.slice(2)): number { - const [command, ...rest] = argv as [CliCommand | undefined, ...string[]]; - - if (!command) { - console.error(usage()); - return 1; - } - - try { - const { flags, positionals } = parseFlags(rest); - if (flags['help'] === 'true') { - console.log(usage()); - return 0; - } - - switch (command) { - case 'vite': - runViteDev(asViteProfile(flags['profile'])); - return 0; - case 'docusaurus': - runDocusaurusDev(); - return 0; - case 'azure-functions': - runAzureFunctionsDev(); - return 0; - case 'tsx': - runTsxDev(asTsxProfile(flags['profile']), { - ...(flags['entry'] ? { entry: flags['entry'] } : {}), - }); - return 0; - case 'azurite': - runAzuriteDev(); - return 0; - case 'sync-api-local-settings': - if (positionals[0] && positionals[0] !== 'e2e') { - throw new Error(`[local-dev] Invalid sync-api-local-settings mode "${positionals[0]}"`); - } - syncApiLocalSettings( - positionals[0] === 'e2e' - ? { - mode: 'e2e', - } - : {}, - ); - return 0; - default: - console.error(usage()); - return 1; - } - } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - return 1; - } -} diff --git a/packages/cellix/local-dev/src/dotenv.ts b/packages/cellix/local-dev/src/dotenv.ts new file mode 100644 index 000000000..5dca2928d --- /dev/null +++ b/packages/cellix/local-dev/src/dotenv.ts @@ -0,0 +1,31 @@ +import { existsSync, readFileSync } from 'node:fs'; + +export type DotEnvValues = Record; + +/** + * Reads a dotenv-style file into a plain key/value object. + * + * Lines without `=` and comment lines are ignored. + */ +export function readDotEnv(filePath: string): DotEnvValues { + if (!existsSync(filePath)) { + return {}; + } + + const values: DotEnvValues = {}; + for (const line of readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const separatorIndex = trimmed.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + values[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1); + } + + return values; +} diff --git a/packages/cellix/local-dev/src/hostnames.ts b/packages/cellix/local-dev/src/hostnames.ts deleted file mode 100644 index 1afa4c8ae..000000000 --- a/packages/cellix/local-dev/src/hostnames.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; - -export interface PortlessHostnames { - uiCommunity: string; - uiStaff: string; - api: string; - mockAuth: string; - docs: string; -} - -export type PortlessHostnameKey = keyof PortlessHostnames; - -interface ResolvePortlessHostnamesOptions extends ResolveWorkspaceRootOptions { - env?: NodeJS.ProcessEnv; -} - -type DotEnvValues = Record; - -export const PORTLESS_PORT = 1355; - -function readDotEnv(filePath: string): DotEnvValues { - if (!existsSync(filePath)) return {}; - - const result: DotEnvValues = {}; - for (const line of readFileSync(filePath, 'utf8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) continue; - - const eqIdx = trimmed.indexOf('='); - if (eqIdx === -1) continue; - result[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); - } - - return result; -} - -function hostnameFrom(url: string): string | null { - try { - return new URL(url).hostname; - } catch { - return null; - } -} - -function hostnameFor(key: string, values: DotEnvValues, env: NodeJS.ProcessEnv): string | null { - return hostnameFrom(env[key] ?? values[key] ?? ''); -} - -function applyWorktreeSuffix(hostname: string, worktreeName: string | undefined): string { - if (!worktreeName) return hostname; - return hostname.replace('.localhost', `.${worktreeName}.localhost`); -} - -/** - * Resolves the local hostnames shared by the main Cellix dev applications. - */ -export function resolvePortlessHostnames(options: ResolvePortlessHostnamesOptions = {}): PortlessHostnames { - const env = options.env ?? process.env; - const workspaceRoot = resolveWorkspaceRoot(options); - const uiEnv = readDotEnv(path.join(workspaceRoot, 'apps', 'ui-community', '.env')); - const staffEnv = readDotEnv(path.join(workspaceRoot, 'apps', 'ui-staff', '.env')); - - const uiCommunity = hostnameFor('VITE_APP_UI_COMMUNITY_BASE_URL', uiEnv, env); - const api = hostnameFor('VITE_COMMON_API_ENDPOINT', uiEnv, env); - const mockAuth = hostnameFor('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', uiEnv, env); - const uiStaff = hostnameFor('VITE_APP_UI_STAFF_AAD_REDIRECT_URI', staffEnv, env); - - if (!uiCommunity || !api || !mockAuth || !uiStaff) { - throw new Error('[local-dev] Could not derive all portless hostnames. Ensure apps/ui-community/.env and apps/ui-staff/.env are present.'); - } - - const worktreeName = env['WORKTREE_NAME']; - const docs = `docs.${uiCommunity}`; - - return { - uiCommunity: applyWorktreeSuffix(uiCommunity, worktreeName), - uiStaff: applyWorktreeSuffix(uiStaff, worktreeName), - api: applyWorktreeSuffix(api, worktreeName), - mockAuth: applyWorktreeSuffix(mockAuth, worktreeName), - docs: applyWorktreeSuffix(docs, worktreeName), - }; -} - -/** - * Builds a portless-proxied HTTPS URL for a hostname and optional path. - */ -export function buildPortlessUrl(hostname: string, relativePath = ''): string { - return `https://${hostname}:${PORTLESS_PORT}${relativePath}`; -} diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index d0bfe34c1..4f79f00f9 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -1,51 +1,32 @@ -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { - applyApiLocalSettingsOverrides, + applyWorktreeSuffix, + buildAzuriteConnectionString, buildPortlessUrl, buildViteArgs, - getAzuriteConnectionString, getAzuritePorts, - getMongoConnectionString, getMongoPort, getWorktreePortOffset, + hostnameFromUrl, PORTLESS_PORT, - resolvePortlessHostnames, + readDotEnv, + replaceUrlPort, resolveWorkspaceRoot, + syncJsonFile, } from '@cellix/local-dev'; import { describe, expect, it } from 'vitest'; +type DotEnvFixtureValues = Record & { + API_URL?: string; + BASE_URL?: string; +}; + function createWorkspaceFixture(): string { const root = mkdtempSync(path.join(tmpdir(), 'cellix-local-dev-')); writeFileSync(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - "apps/*"\n'); - mkdirSync(path.join(root, 'apps', 'ui-community'), { recursive: true }); - mkdirSync(path.join(root, 'apps', 'ui-staff'), { recursive: true }); - mkdirSync(path.join(root, 'apps', 'api'), { recursive: true }); - - writeFileSync( - path.join(root, 'apps', 'ui-community', '.env'), - [ - 'VITE_APP_UI_COMMUNITY_BASE_URL=https://ownercommunity.localhost:1355', - 'VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql', - 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/community', - ].join('\n'), - ); - writeFileSync(path.join(root, 'apps', 'ui-staff', '.env'), 'VITE_APP_UI_STAFF_AAD_REDIRECT_URI=https://staff.ownercommunity.localhost:1355/auth-redirect\n'); - writeFileSync( - path.join(root, 'apps', 'api', 'local.settings.json'), - JSON.stringify( - { - Values: { - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/test?replicaSet=rs0', - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - }, - }, - null, - 2, - ), - ); + mkdirSync(path.join(root, 'fixtures'), { recursive: true }); return root; } @@ -53,27 +34,33 @@ function createWorkspaceFixture(): string { describe('@cellix/local-dev', () => { it('resolves the workspace root from a nested directory', () => { const workspaceRoot = createWorkspaceFixture(); - const nestedDir = path.join(workspaceRoot, 'apps', 'ui-community'); + const nestedDir = path.join(workspaceRoot, 'fixtures'); expect(resolveWorkspaceRoot({ startDir: nestedDir })).toBe(workspaceRoot); }); - it('derives shared hostnames and applies the worktree suffix', () => { + it('parses dotenv values and applies worktree-aware URL helpers generically', () => { const workspaceRoot = createWorkspaceFixture(); + const envPath = path.join(workspaceRoot, 'fixtures', '.env'); + writeFileSync(envPath, ['BASE_URL=https://ownercommunity.localhost:1355', 'API_URL=https://data-access.ownercommunity.localhost:1355/api/graphql'].join('\n')); - expect( - resolvePortlessHostnames({ - startDir: path.join(workspaceRoot, 'apps', 'ui-community'), - env: { WORKTREE_NAME: 'feature-123' }, - }), - ).toEqual({ - uiCommunity: 'ownercommunity.feature-123.localhost', - uiStaff: 'staff.ownercommunity.feature-123.localhost', - api: 'data-access.ownercommunity.feature-123.localhost', - mockAuth: 'mock-auth.ownercommunity.feature-123.localhost', - docs: 'docs.ownercommunity.feature-123.localhost', + const envValues = readDotEnv(envPath) as DotEnvFixtureValues; + const baseUrl = envValues.BASE_URL; + const apiUrl = envValues.API_URL; + + expect(envValues).toEqual({ + BASE_URL: 'https://ownercommunity.localhost:1355', + API_URL: 'https://data-access.ownercommunity.localhost:1355/api/graphql', }); + expect(baseUrl).toBe('https://ownercommunity.localhost:1355'); + expect(apiUrl).toBe('https://data-access.ownercommunity.localhost:1355/api/graphql'); + if (!baseUrl || !apiUrl) { + throw new Error('Expected dotenv fixture values to be defined'); + } + expect(hostnameFromUrl(baseUrl)).toBe('ownercommunity.localhost'); + expect(applyWorktreeSuffix('ownercommunity.localhost', 'feature-123')).toBe('ownercommunity.feature-123.localhost'); expect(buildPortlessUrl('ownercommunity.localhost')).toBe(`https://ownercommunity.localhost:${PORTLESS_PORT}`); + expect(replaceUrlPort(apiUrl, 50900)).toBe('https://data-access.ownercommunity.localhost:50900/api/graphql'); }); it('builds shared Vite args including e2e mode', () => { @@ -87,9 +74,6 @@ describe('@cellix/local-dev', () => { }); it('derives deterministic worktree ports and connection strings', () => { - const workspaceRoot = createWorkspaceFixture(); - const env = { WORKTREE_NAME: 'feature-123' }; - expect(getWorktreePortOffset('feature-123')).toBeGreaterThanOrEqual(100); expect(getMongoPort('feature-123')).toBe(50000 + getWorktreePortOffset('feature-123')); expect(getAzuritePorts('feature-123')).toEqual({ @@ -98,40 +82,47 @@ describe('@cellix/local-dev', () => { table: 10002 + getWorktreePortOffset('feature-123'), }); expect( - getMongoConnectionString({ - startDir: path.join(workspaceRoot, 'apps', 'api'), - env, - }), - ).toContain(`:${getMongoPort('feature-123')}/test?replicaSet=rs0`); - expect( - getAzuriteConnectionString({ - startDir: path.join(workspaceRoot, 'apps', 'api'), - env, + buildAzuriteConnectionString({ + accountKey: 'key', + accountName: 'devstoreaccount1', + ports: getAzuritePorts('feature-123'), }), ).toContain(`BlobEndpoint=http://127.0.0.1:${getAzuritePorts('feature-123').blob}/devstoreaccount1`); }); - it('applies worktree and runtime overrides to API local settings', () => { + it('syncs json files through a consumer-supplied transform', () => { const workspaceRoot = createWorkspaceFixture(); - const settings = { - Values: { - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', - }, - }; + const sourcePath = path.join(workspaceRoot, 'fixtures', 'source.json'); + const targetPath = path.join(workspaceRoot, 'fixtures', 'target', 'settings.json'); + writeFileSync( + sourcePath, + JSON.stringify( + { + Values: { + MODE: 'local', + }, + }, + null, + 2, + ), + ); - const updated = applyApiLocalSettingsOverrides(settings, { - workspaceRoot, - env: { - WORKTREE_NAME: 'feature-123', - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:61234/override?replicaSet=rs0', - }, + syncJsonFile({ + sourcePath, + targetPath, + transform: (document: { Values?: Record }) => ({ + ...document, + Values: { + ...(document.Values ?? {}), + MODE: 'e2e', + }, + }), }); - expect(updated.Values?.['ACCOUNT_PORTAL_OIDC_ISSUER']).toBe('https://mock-auth.ownercommunity.feature-123.localhost:1355/community'); - expect(updated.Values?.['STAFF_PORTAL_OIDC_ENDPOINT']).toBe('https://mock-auth.ownercommunity.feature-123.localhost:1355/staff/.well-known/jwks.json'); - expect(updated.Values?.['COSMOSDB_CONNECTION_STRING']).toBe('mongodb://127.0.0.1:61234/override?replicaSet=rs0'); - expect(updated.Values?.['AZURE_STORAGE_CONNECTION_STRING']).not.toBe('UseDevelopmentStorage=true'); + expect(JSON.parse(readFileSync(targetPath, 'utf8'))).toEqual({ + Values: { + MODE: 'e2e', + }, + }); }); }); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index 2cdaacbbf..53edbb87e 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,31 +1,38 @@ -export { applyApiLocalSettingsOverrides, syncApiLocalSettings } from './api-local-settings.ts'; -export { runCli } from './cli.ts'; export { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; export { - buildPortlessUrl, - PORTLESS_PORT, - type PortlessHostnameKey, - type PortlessHostnames, - resolvePortlessHostnames, -} from './hostnames.ts'; + type DotEnvValues, + readDotEnv, +} from './dotenv.ts'; +export { + readJsonFile, + type SyncJsonFileOptions, + syncJsonFile, + writeJsonFile, +} from './json-files.ts'; export { + type AzureFunctionsDevOptions, + type AzuriteDevOptions, type RunnerOptions, runAzureFunctionsDev, runAzuriteDev, runDocusaurusDev, runTsxDev, runViteDev, - type TsxDevProfile, - type ViteDevProfile, + type TsxRunnerOptions, } from './runners.ts'; +export { + applyWorktreeSuffix, + buildPortlessUrl, + hostnameFromUrl, + PORTLESS_PORT, + replaceUrlPort, +} from './urls.ts'; export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite.ts'; export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; export { type AzuritePorts, - type ConnectionStringOptions, - getAzuriteConnectionString, + buildAzuriteConnectionString, getAzuritePorts, - getMongoConnectionString, getMongoPort, getWorktreePortOffset, } from './worktree-ports.ts'; diff --git a/packages/cellix/local-dev/src/json-files.ts b/packages/cellix/local-dev/src/json-files.ts new file mode 100644 index 000000000..4c9c5296c --- /dev/null +++ b/packages/cellix/local-dev/src/json-files.ts @@ -0,0 +1,42 @@ +import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +export interface SyncJsonFileOptions { + sourcePath: string; + targetPath: string; + transform?: (data: TData) => TData; +} + +/** + * Reads a JSON file and returns its parsed contents. + */ +export function readJsonFile(filePath: string): TData { + return JSON.parse(readFileSync(filePath, 'utf8')) as TData; +} + +/** + * Writes a JSON document with stable indentation and a trailing newline. + */ +export function writeJsonFile(filePath: string, data: unknown): void { + mkdirSync(path.dirname(filePath), { recursive: true }); + writeFileSync(filePath, `${JSON.stringify(data, null, '\t')}\n`); +} + +/** + * Copies a JSON file to a target location and optionally applies a transform. + */ +export function syncJsonFile(options: SyncJsonFileOptions): void { + mkdirSync(path.dirname(options.targetPath), { recursive: true }); + + if (!options.transform) { + copyFileSync(options.sourcePath, options.targetPath); + return; + } + + if (!existsSync(options.sourcePath)) { + throw new Error(`[local-dev] Missing JSON source file: ${options.sourcePath}`); + } + + const sourceData = readJsonFile(options.sourcePath); + writeJsonFile(options.targetPath, options.transform(sourceData)); +} diff --git a/packages/cellix/local-dev/src/runners.ts b/packages/cellix/local-dev/src/runners.ts index aecd430ac..1afa324f1 100644 --- a/packages/cellix/local-dev/src/runners.ts +++ b/packages/cellix/local-dev/src/runners.ts @@ -2,23 +2,39 @@ import { type ChildProcess, spawn } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; -import { buildPortlessUrl, type PortlessHostnameKey, resolvePortlessHostnames } from './hostnames.ts'; import { buildViteArgs } from './vite.ts'; -import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; -import { getAzuriteConnectionString, getAzuritePorts, getMongoConnectionString, getMongoPort } from './worktree-ports.ts'; -export type ViteDevProfile = 'ui-community' | 'ui-staff'; -export type TsxDevProfile = 'oauth2-mock' | 'mongo-memory-mock'; +type RunnerEnv = NodeJS.ProcessEnv & { + HOST?: string; + NODE_OPTIONS?: string; + PORT?: string; + PORTLESS_CA_PATH?: string; +}; -export interface RunnerOptions extends ResolveWorkspaceRootOptions { +export interface RunnerOptions { env?: NodeJS.ProcessEnv; } -interface TsxRunnerOptions extends RunnerOptions { +export interface TsxRunnerOptions extends RunnerOptions { entry?: string; } -type OverrideSpec = Record; +export interface AzureFunctionsDevOptions extends RunnerOptions { + port?: string; + scriptRoot?: string; + cors?: string; + typescript?: boolean; +} + +export interface AzuriteDevOptions extends RunnerOptions { + blobPort: number; + queuePort: number; + tablePort: number; + blobLocation: string; + queueLocation: string; + tableLocation: string; + silent?: boolean; +} function spawnInherited(command: string, args: string[], options: { env?: NodeJS.ProcessEnv } = {}): ChildProcess { return spawn(command, args, { @@ -27,67 +43,19 @@ function spawnInherited(command: string, args: string[], options: { env?: NodeJS }); } -function applyPortlessEnvOverrides(childEnv: NodeJS.ProcessEnv, overrideSpec: OverrideSpec, options: RunnerOptions, settings: { preserveExisting?: boolean } = {}): void { - const env = options.env ?? process.env; - if (!env['WORKTREE_NAME']) return; - - const hostnames = resolvePortlessHostnames(options); - for (const [key, target] of Object.entries(overrideSpec)) { - if (settings.preserveExisting && childEnv[key]) { - continue; - } - - childEnv[key] = buildPortlessUrl(hostnames[target.hostname], target.path ?? ''); - } -} - /** - * Starts a Vite dev process using one of the shared Cellix UI profiles. + * Starts a Vite dev process using the caller-provided environment. */ -export function runViteDev(profile: ViteDevProfile, options: RunnerOptions = {}): ChildProcess { - const env = options.env ?? process.env; - const childEnv = { ...env }; - - if (profile === 'ui-community') { - applyPortlessEnvOverrides( - childEnv, - { - VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: { - hostname: 'mockAuth', - path: '/community', - }, - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: { - hostname: 'uiCommunity', - path: '/auth-redirect', - }, - VITE_COMMON_API_ENDPOINT: { hostname: 'api', path: '/api/graphql' }, - VITE_APP_UI_COMMUNITY_BASE_URL: { hostname: 'uiCommunity' }, - }, - options, - ); - } else { - applyPortlessEnvOverrides( - childEnv, - { - VITE_APP_UI_STAFF_AAD_AUTHORITY: { hostname: 'mockAuth', path: '/staff' }, - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: { - hostname: 'uiStaff', - path: '/auth-redirect', - }, - VITE_COMMON_API_ENDPOINT: { hostname: 'api', path: '/api/graphql' }, - }, - options, - ); - } - +export function runViteDev(options: RunnerOptions = {}): ChildProcess { + const env = (options.env ?? process.env) as RunnerEnv; const child = spawnInherited( 'vite', buildViteArgs({ - ...(env['HOST'] ? { host: env['HOST'] } : {}), - ...(env['PORT'] ? { port: env['PORT'] } : {}), + ...(env.HOST ? { host: env.HOST } : {}), + ...(env.PORT ? { port: env.PORT } : {}), env, }), - { env: childEnv }, + { env }, ); forwardChildExit(child); return child; @@ -97,18 +65,19 @@ export function runViteDev(profile: ViteDevProfile, options: RunnerOptions = {}) * Starts the Docusaurus dev server with the shared local-dev defaults. */ export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { - const env = options.env ?? process.env; - const child = spawnInherited('docusaurus', ['start', '--port', env['PORT'] ?? '3001', '--host', '127.0.0.1', '--no-open']); + const env = (options.env ?? process.env) as RunnerEnv; + const child = spawnInherited('docusaurus', ['start', '--port', env.PORT ?? '3001', '--host', '127.0.0.1', '--no-open'], { env }); forwardChildExit(child); return child; } /** - * Starts the API Azure Functions process with shared CA, CORS, and worktree overrides. + * Starts an Azure Functions dev process using caller-supplied runtime + * configuration and environment variables. */ -export function runAzureFunctionsDev(options: RunnerOptions = {}): ChildProcess { - const env = options.env ?? process.env; - const envPort = env['PORT']; +export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): ChildProcess { + const env = (options.env ?? process.env) as RunnerEnv; + const envPort = options.port ?? env.PORT; if (!envPort) { throw new Error('[local-dev] PORT environment variable is not set. Start this command through portless.'); @@ -116,88 +85,46 @@ export function runAzureFunctionsDev(options: RunnerOptions = {}): ChildProcess const childEnv: NodeJS.ProcessEnv = { ...env, - NODE_EXTRA_CA_CERTS: env['PORTLESS_CA_PATH'] ?? path.join(os.homedir(), '.portless', 'ca.pem'), - NODE_OPTIONS: `${env['NODE_OPTIONS'] ?? ''} --use-system-ca`.trim(), + NODE_EXTRA_CA_CERTS: env.PORTLESS_CA_PATH ?? path.join(os.homedir(), '.portless', 'ca.pem'), + NODE_OPTIONS: `${env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), }; - if (env['WORKTREE_NAME']) { - applyPortlessEnvOverrides( - childEnv, - { - ACCOUNT_PORTAL_OIDC_ISSUER: { hostname: 'mockAuth', path: '/community' }, - ACCOUNT_PORTAL_OIDC_ENDPOINT: { - hostname: 'mockAuth', - path: '/community/.well-known/jwks.json', - }, - STAFF_PORTAL_OIDC_ISSUER: { hostname: 'mockAuth', path: '/staff' }, - STAFF_PORTAL_OIDC_ENDPOINT: { - hostname: 'mockAuth', - path: '/staff/.well-known/jwks.json', - }, - }, - options, - { preserveExisting: true }, - ); - childEnv['COSMOSDB_CONNECTION_STRING'] ??= getMongoConnectionString(options); - childEnv['AZURE_STORAGE_CONNECTION_STRING'] ??= getAzuriteConnectionString(options); - childEnv['AzureWebJobsStorage'] ??= getAzuriteConnectionString(options); - childEnv['languageWorkers__node__arguments'] ??= ''; + const args = ['start']; + if (options.typescript ?? true) { + args.push('--typescript'); } - const child = spawnInherited('func', ['start', '--typescript', '--script-root', 'deploy/', '--port', envPort, '--cors', '*'], { env: childEnv }); + args.push('--script-root', options.scriptRoot ?? 'deploy/', '--port', envPort, '--cors', options.cors ?? '*'); + + const child = spawnInherited('func', args, { env: childEnv }); forwardChildExit(child); return child; } /** - * Starts a TSX-backed mock service using one of the shared Cellix profiles. + * Starts a TSX-backed dev process using the caller-provided entrypoint and + * environment. */ -export function runTsxDev(profile: TsxDevProfile, options: TsxRunnerOptions = {}): ChildProcess { +export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { const env = options.env ?? process.env; - const childEnv: NodeJS.ProcessEnv = { ...env }; - - if (profile === 'oauth2-mock') { - applyPortlessEnvOverrides( - childEnv, - { - BASE_URL: { hostname: 'mockAuth' }, - VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: { - hostname: 'uiCommunity', - path: '/auth-redirect', - }, - VITE_APP_UI_STAFF_AAD_REDIRECT_URI: { - hostname: 'uiStaff', - path: '/auth-redirect', - }, - }, - options, - ); - } else if (profile === 'mongo-memory-mock') { - childEnv['PORT'] = String(getMongoPort(env['WORKTREE_NAME'])); - } - - const child = spawnInherited('tsx', [options.entry ?? 'src/index.ts'], { env: childEnv }); + const child = spawnInherited('tsx', [options.entry ?? 'src/index.ts'], { env }); forwardChildExit(child); return child; } /** - * Starts the three Azurite worker processes with worktree-scoped ports and storage paths. + * Starts the three Azurite worker processes using caller-supplied ports and + * storage paths. */ -export function runAzuriteDev(options: RunnerOptions = {}): ChildProcess[] { - const env = options.env ?? process.env; - const workspaceRoot = resolveWorkspaceRoot(options); - const ports = getAzuritePorts(env['WORKTREE_NAME']); - const storageSuffix = env['WORKTREE_NAME'] ? `-${env['WORKTREE_NAME']}` : ''; - +export function runAzuriteDev(options: AzuriteDevOptions): ChildProcess[] { const procSpecs: Array<[string, string[]]> = [ - ['azurite-blob', ['--silent', '--blobPort', String(ports.blob), '--location', path.join(workspaceRoot, `__blobstorage__${storageSuffix}`)]], - ['azurite-queue', ['--silent', '--queuePort', String(ports.queue), '--location', path.join(workspaceRoot, `__queuestorage__${storageSuffix}`)]], - ['azurite-table', ['--silent', '--tablePort', String(ports.table), '--location', path.join(workspaceRoot, `__tablestorage__${storageSuffix}`)]], + ['azurite-blob', [...((options.silent ?? true) ? ['--silent'] : []), '--blobPort', String(options.blobPort), '--location', path.resolve(options.blobLocation)]], + ['azurite-queue', [...((options.silent ?? true) ? ['--silent'] : []), '--queuePort', String(options.queuePort), '--location', path.resolve(options.queueLocation)]], + ['azurite-table', [...((options.silent ?? true) ? ['--silent'] : []), '--tablePort', String(options.tablePort), '--location', path.resolve(options.tableLocation)]], ]; const procs = procSpecs.map(([command, args]) => { - const proc = spawnInherited(command, args); + const proc = spawnInherited(command, args, options.env ? { env: options.env } : {}); proc.on('error', (error) => { console.error(`[azurite] failed to start ${command}: ${error.message}`); for (const runningProc of procs) { @@ -208,7 +135,7 @@ export function runAzuriteDev(options: RunnerOptions = {}): ChildProcess[] { return proc; }); - console.log(`[azurite] started (blob=${ports.blob}, queue=${ports.queue}, table=${ports.table})`); + console.log(`[azurite] started (blob=${options.blobPort}, queue=${options.queuePort}, table=${options.tablePort})`); let exited = 0; for (const proc of procs) { diff --git a/packages/cellix/local-dev/src/urls.ts b/packages/cellix/local-dev/src/urls.ts new file mode 100644 index 000000000..8b4b71b73 --- /dev/null +++ b/packages/cellix/local-dev/src/urls.ts @@ -0,0 +1,40 @@ +export const PORTLESS_PORT = 1355; + +/** + * Returns the hostname portion of a URL string, or `null` when the value is not + * a valid URL. + */ +export function hostnameFromUrl(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +/** + * Applies a worktree suffix to a `.localhost` hostname. + */ +export function applyWorktreeSuffix(hostname: string, worktreeName: string | undefined): string { + if (!worktreeName) { + return hostname; + } + + return hostname.replace('.localhost', `.${worktreeName}.localhost`); +} + +/** + * Builds a portless-proxied HTTPS URL for a hostname and optional path. + */ +export function buildPortlessUrl(hostname: string, relativePath = ''): string { + return `https://${hostname}:${PORTLESS_PORT}${relativePath}`; +} + +/** + * Replaces the port component of a URL and returns the updated string. + */ +export function replaceUrlPort(url: string, port: number | string): string { + const parsedUrl = new URL(url); + parsedUrl.port = String(port); + return parsedUrl.toString(); +} diff --git a/packages/cellix/local-dev/src/vite.ts b/packages/cellix/local-dev/src/vite.ts index 7944136f1..40c9b0b15 100644 --- a/packages/cellix/local-dev/src/vite.ts +++ b/packages/cellix/local-dev/src/vite.ts @@ -4,11 +4,18 @@ export interface BuildViteArgsOptions { env?: NodeJS.ProcessEnv; } +type ViteEnv = NodeJS.ProcessEnv & { + E2E?: string; + E2E_VITE_MODE?: string; + TF_BUILD?: string; +}; + /** * Returns true when the current process is running in an e2e-oriented mode. */ export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { - return ['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()); + const viteEnv = env as ViteEnv; + return ['1', 'true', 'yes'].includes((viteEnv.E2E ?? '').toLowerCase()); } /** @@ -16,13 +23,14 @@ export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { */ export function buildViteArgs(options: BuildViteArgsOptions = {}): string[] { const { host = '127.0.0.1', port, env = process.env } = options; + const viteEnv = env as ViteEnv; const args = ['--host', host]; if (port) { args.push('--port', port); } - const viteMode = env['E2E_VITE_MODE'] ?? (isE2E(env) || env['TF_BUILD'] ? 'e2e' : undefined); + const viteMode = viteEnv.E2E_VITE_MODE ?? (isE2E(viteEnv) || viteEnv.TF_BUILD ? 'e2e' : undefined); if (viteMode) { args.push('--mode', viteMode); } diff --git a/packages/cellix/local-dev/src/worktree-ports.ts b/packages/cellix/local-dev/src/worktree-ports.ts index d9b4ff806..493c20fb3 100644 --- a/packages/cellix/local-dev/src/worktree-ports.ts +++ b/packages/cellix/local-dev/src/worktree-ports.ts @@ -1,42 +1,21 @@ -import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; - -type SettingsValues = Record; - export interface AzuritePorts { blob: number; queue: number; table: number; } -export interface ConnectionStringOptions extends ResolveWorkspaceRootOptions { - env?: NodeJS.ProcessEnv; - values?: SettingsValues; -} +type WorktreeEnv = NodeJS.ProcessEnv & { + WORKTREE_NAME?: string; +}; -function readApiLocalSettingsValues(workspaceRoot: string): SettingsValues { - const candidatePaths = [path.join(workspaceRoot, 'apps', 'api', 'deploy', 'local.settings.json'), path.join(workspaceRoot, 'apps', 'api', 'local.settings.json')]; - - for (const settingsPath of candidatePaths) { - if (!existsSync(settingsPath)) continue; - const settings = JSON.parse(readFileSync(settingsPath, 'utf8')) as { - Values?: SettingsValues; - }; - return settings.Values ?? {}; - } - - return {}; -} - -function getSetting(name: string, options: ConnectionStringOptions, workspaceRoot: string): string | undefined { - return options.env?.[name] ?? options.values?.[name] ?? readApiLocalSettingsValues(workspaceRoot)[name]; +function getDefaultWorktreeName(): string | undefined { + return (process.env as WorktreeEnv).WORKTREE_NAME; } /** * Returns a deterministic worktree port offset in increments of 100. */ -export function getWorktreePortOffset(worktreeName = process.env['WORKTREE_NAME']): number { +export function getWorktreePortOffset(worktreeName = getDefaultWorktreeName()): number { if (!worktreeName) return 0; let hash = 0; @@ -50,14 +29,14 @@ export function getWorktreePortOffset(worktreeName = process.env['WORKTREE_NAME' /** * Returns the MongoDB port for the current worktree. */ -export function getMongoPort(worktreeName = process.env['WORKTREE_NAME']): number { +export function getMongoPort(worktreeName = getDefaultWorktreeName()): number { return 50000 + getWorktreePortOffset(worktreeName); } /** * Returns the Azurite ports for the current worktree. */ -export function getAzuritePorts(worktreeName = process.env['WORKTREE_NAME']): AzuritePorts { +export function getAzuritePorts(worktreeName = getDefaultWorktreeName()): AzuritePorts { const offset = getWorktreePortOffset(worktreeName); return { @@ -68,40 +47,17 @@ export function getAzuritePorts(worktreeName = process.env['WORKTREE_NAME']): Az } /** - * Returns the Azurite connection string for the current worktree. + * Builds an Azurite connection string from explicit account credentials and + * ports supplied by the consumer. */ -export function getAzuriteConnectionString(options: ConnectionStringOptions = {}): string { - const workspaceRoot = resolveWorkspaceRoot(options); - const ports = getAzuritePorts(options.env?.['WORKTREE_NAME'] ?? process.env['WORKTREE_NAME']); - if (ports.blob === 10000) return 'UseDevelopmentStorage=true'; - - const accountName = getSetting('STORAGE_ACCOUNT_NAME', options, workspaceRoot); - const accountKey = getSetting('STORAGE_ACCOUNT_KEY', options, workspaceRoot); - if (!accountName || !accountKey) { - throw new Error('[local-dev] STORAGE_ACCOUNT_NAME and STORAGE_ACCOUNT_KEY must be set to build a worktree Azurite connection string'); - } - +export function buildAzuriteConnectionString(options: { accountName: string; accountKey: string; ports: AzuritePorts; host?: string }): string { + const host = options.host ?? '127.0.0.1'; return [ 'DefaultEndpointsProtocol=http', - `AccountName=${accountName}`, - `AccountKey=${accountKey}`, - `BlobEndpoint=http://127.0.0.1:${ports.blob}/${accountName}`, - `QueueEndpoint=http://127.0.0.1:${ports.queue}/${accountName}`, - `TableEndpoint=http://127.0.0.1:${ports.table}/${accountName}`, + `AccountName=${options.accountName}`, + `AccountKey=${options.accountKey}`, + `BlobEndpoint=http://${host}:${options.ports.blob}/${options.accountName}`, + `QueueEndpoint=http://${host}:${options.ports.queue}/${options.accountName}`, + `TableEndpoint=http://${host}:${options.ports.table}/${options.accountName}`, ].join(';'); } - -/** - * Reads the API Mongo connection string and patches in the worktree-specific port. - */ -export function getMongoConnectionString(options: ConnectionStringOptions = {}): string { - const workspaceRoot = resolveWorkspaceRoot(options); - const base = getSetting('COSMOSDB_CONNECTION_STRING', options, workspaceRoot); - if (!base) { - throw new Error('[local-dev] COSMOSDB_CONNECTION_STRING must be set'); - } - - const url = new URL(base); - url.port = String(getMongoPort(options.env?.['WORKTREE_NAME'] ?? process.env['WORKTREE_NAME'])); - return url.toString(); -} diff --git a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts index 511fc56e0..59ac1efed 100644 --- a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts @@ -12,6 +12,7 @@ const currentDir = fileURLToPath(new URL('.', import.meta.url)); /** Register the Cucumber Before/After/AfterAll and screenshot hooks for the E2E suite. */ export function registerLifecycleHooks(): void { registerWorldLifecycleHooks({ + beforeTimeout: getTimeout('serverStartup') + getTimeout('uiInit') * 3, scenarioTimeout: getTimeout('scenario'), before: async (world) => { await world.init(); diff --git a/packages/ocom-verification/e2e-tests/src/infrastructure.ts b/packages/ocom-verification/e2e-tests/src/infrastructure.ts index 89d401faa..6a20da5ca 100644 --- a/packages/ocom-verification/e2e-tests/src/infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/infrastructure.ts @@ -18,7 +18,9 @@ const infrastructure = E2EInfrastructure.create({ }) .addServer('azurite', () => createTestAzuriteServer()) .addServer('auth', () => createTestOAuth2Server()) - .addServer('api', (ctx) => createTestApiServer(() => ctx.server('mongo').getConnectionString()), { dependsOn: ['mongo'] }) + .addServer('api', (ctx) => createTestApiServer(() => ctx.server('mongo').getConnectionString()), { + dependsOn: ['mongo', 'azurite'], + }) .addUiPortal('community', () => createCommunityUiPortalServer()) .addUiPortal('staff', () => createStaffUiPortalServer()); diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts index cfb488e4c..5c3383122 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts @@ -43,9 +43,23 @@ export function initTestEnvironment() { timeout: 10_000, stdio: 'pipe', }); + try { + execFileSync(getPortlessPath(), ['proxy', 'stop', '-p', '1355'], { + timeout: 10_000, + stdio: 'pipe', + }); + } catch { + // It's fine if no proxy was already running on the test port. + } execFileSync(getPortlessPath(), ['proxy', 'start', '--https', '-p', '1355'], { timeout: 15_000, stdio: 'pipe', + env: { + ...process.env, + // E2E needs exact host matches so portal probes do not mistake the + // community app for the staff app via wildcard subdomain fallback. + PORTLESS_WILDCARD: '0', + }, }); proxyInitialized = true; diff --git a/packages/ocom/local-dev-config/.gitignore b/packages/ocom/local-dev-config/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/packages/ocom/local-dev-config/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/ocom/local-dev-config/package.json b/packages/ocom/local-dev-config/package.json new file mode 100644 index 000000000..0f60c1930 --- /dev/null +++ b/packages/ocom/local-dev-config/package.json @@ -0,0 +1,47 @@ +{ + "name": "@ocom/local-dev-config", + "version": "1.0.0", + "description": "OCOM-specific local-development hostname/URL helpers built on @cellix/local-dev", + "type": "module", + "files": [ + "dist", + "src" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./hostnames": { + "types": "./dist/hostnames.d.ts", + "default": "./dist/hostnames.js" + }, + "./urls": { + "types": "./dist/urls.d.ts", + "default": "./dist/urls.js" + }, + "./workspace": { + "types": "./dist/workspace.d.ts", + "default": "./dist/workspace.js" + } + }, + "scripts": { + "prebuild": "pnpm run lint", + "build": "tsgo --build", + "clean": "rimraf dist", + "lint": "biome lint", + "format": "biome format --write", + "format:check": "biome format ." + }, + "dependencies": { + "@cellix/local-dev": "workspace:*" + }, + "devDependencies": { + "@cellix/config-typescript": "workspace:*", + "@types/node": "catalog:", + "rimraf": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/ocom/local-dev-config/src/hostnames.ts b/packages/ocom/local-dev-config/src/hostnames.ts new file mode 100644 index 000000000..191cb173e --- /dev/null +++ b/packages/ocom/local-dev-config/src/hostnames.ts @@ -0,0 +1,51 @@ +import path from 'node:path'; +import { applyWorktreeSuffix, type DotEnvValues, hostnameFromUrl, readDotEnv } from '@cellix/local-dev'; +import type { OcomHostnames, OcomLocalDevOptions } from './types.ts'; +import { getWorkspaceRoot } from './workspace.ts'; + +interface OcomEnvValues { + WORKTREE_NAME?: string; + VITE_APP_UI_COMMUNITY_BASE_URL?: string; + VITE_COMMON_API_ENDPOINT?: string; + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY?: string; + VITE_APP_UI_STAFF_BASE_URL?: string; + VITE_APP_UI_STAFF_AAD_REDIRECT_URI?: string; +} + +function requiredHostname(url: string, key: string): string { + const hostname = hostnameFromUrl(url); + if (!hostname) { + throw new Error(`[ocom-local-dev] Missing or invalid URL for ${key}`); + } + + return hostname; +} + +function readAppEnv(workspaceRoot: string, appName: string): DotEnvValues & OcomEnvValues { + return readDotEnv(path.join(workspaceRoot, 'apps', appName, '.env')) as DotEnvValues & OcomEnvValues; +} + +export function getOcomHostnames(options: OcomLocalDevOptions = {}): OcomHostnames { + const env = (options.env ?? process.env) as NodeJS.ProcessEnv & OcomEnvValues; + const workspaceRoot = options.workspaceRoot ?? getWorkspaceRoot(); + const communityEnv = readAppEnv(workspaceRoot, 'ui-community'); + const staffEnv = readAppEnv(workspaceRoot, 'ui-staff'); + const worktreeName = env.WORKTREE_NAME; + const communityHostname = requiredHostname(env.VITE_APP_UI_COMMUNITY_BASE_URL ?? communityEnv.VITE_APP_UI_COMMUNITY_BASE_URL ?? '', 'VITE_APP_UI_COMMUNITY_BASE_URL'); + const apiHostname = requiredHostname(env.VITE_COMMON_API_ENDPOINT ?? communityEnv.VITE_COMMON_API_ENDPOINT ?? '', 'VITE_COMMON_API_ENDPOINT'); + const mockAuthHostname = requiredHostname(env.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY ?? communityEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY ?? '', 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'); + const staffHostname = requiredHostname( + env.VITE_APP_UI_STAFF_BASE_URL ?? staffEnv.VITE_APP_UI_STAFF_BASE_URL ?? env.VITE_APP_UI_STAFF_AAD_REDIRECT_URI ?? staffEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI ?? '', + 'VITE_APP_UI_STAFF_BASE_URL', + ); + + return { + uiCommunity: applyWorktreeSuffix(communityHostname, worktreeName), + uiStaff: applyWorktreeSuffix(staffHostname, worktreeName), + api: applyWorktreeSuffix(apiHostname, worktreeName), + mockAuth: applyWorktreeSuffix(mockAuthHostname, worktreeName), + docs: applyWorktreeSuffix(`docs.${communityHostname}`, worktreeName), + }; +} + +export type { OcomHostnames, OcomLocalDevOptions } from './types.ts'; diff --git a/packages/ocom/local-dev-config/src/index.ts b/packages/ocom/local-dev-config/src/index.ts new file mode 100644 index 000000000..93cbebc32 --- /dev/null +++ b/packages/ocom/local-dev-config/src/index.ts @@ -0,0 +1,4 @@ +export { getOcomHostnames } from './hostnames.ts'; +export type { OcomHostnames, OcomLocalDevOptions, OcomUrls } from './types.ts'; +export { buildOcomUrls } from './urls.ts'; +export { getWorkspaceRoot } from './workspace.ts'; diff --git a/packages/ocom/local-dev-config/src/types.ts b/packages/ocom/local-dev-config/src/types.ts new file mode 100644 index 000000000..b862ea512 --- /dev/null +++ b/packages/ocom/local-dev-config/src/types.ts @@ -0,0 +1,25 @@ +export interface OcomLocalDevOptions { + env?: NodeJS.ProcessEnv; + workspaceRoot?: string; +} + +export interface OcomHostnames { + uiCommunity: string; + uiStaff: string; + api: string; + mockAuth: string; + docs: string; +} + +export interface OcomUrls { + uiCommunityBaseUrl: string; + uiCommunityRedirectUrl: string; + uiStaffBaseUrl: string; + uiStaffRedirectUrl: string; + apiGraphqlUrl: string; + mockCommunityAuthorityUrl: string; + mockCommunityJwksUrl: string; + mockStaffAuthorityUrl: string; + mockStaffJwksUrl: string; + docsBaseUrl: string; +} diff --git a/packages/ocom/local-dev-config/src/urls.ts b/packages/ocom/local-dev-config/src/urls.ts new file mode 100644 index 000000000..2302d5d32 --- /dev/null +++ b/packages/ocom/local-dev-config/src/urls.ts @@ -0,0 +1,22 @@ +import { buildPortlessUrl } from '@cellix/local-dev'; +import { getOcomHostnames } from './hostnames.ts'; +import type { OcomLocalDevOptions, OcomUrls } from './types.ts'; + +export function buildOcomUrls(options: OcomLocalDevOptions = {}): OcomUrls { + const hostnames = getOcomHostnames(options); + + return { + uiCommunityBaseUrl: buildPortlessUrl(hostnames.uiCommunity), + uiCommunityRedirectUrl: buildPortlessUrl(hostnames.uiCommunity, '/auth-redirect'), + uiStaffBaseUrl: buildPortlessUrl(hostnames.uiStaff), + uiStaffRedirectUrl: buildPortlessUrl(hostnames.uiStaff, '/auth-redirect'), + apiGraphqlUrl: buildPortlessUrl(hostnames.api, '/api/graphql'), + mockCommunityAuthorityUrl: buildPortlessUrl(hostnames.mockAuth, '/community'), + mockCommunityJwksUrl: buildPortlessUrl(hostnames.mockAuth, '/community/.well-known/jwks.json'), + mockStaffAuthorityUrl: buildPortlessUrl(hostnames.mockAuth, '/staff'), + mockStaffJwksUrl: buildPortlessUrl(hostnames.mockAuth, '/staff/.well-known/jwks.json'), + docsBaseUrl: buildPortlessUrl(hostnames.docs), + }; +} + +export type { OcomLocalDevOptions, OcomUrls } from './types.ts'; diff --git a/packages/ocom/local-dev-config/src/workspace.ts b/packages/ocom/local-dev-config/src/workspace.ts new file mode 100644 index 000000000..2c66b424e --- /dev/null +++ b/packages/ocom/local-dev-config/src/workspace.ts @@ -0,0 +1,5 @@ +import { resolveWorkspaceRoot } from '@cellix/local-dev'; + +export function getWorkspaceRoot(startDir: string = process.cwd()): string { + return resolveWorkspaceRoot({ startDir }); +} diff --git a/packages/ocom/local-dev-config/tsconfig.json b/packages/ocom/local-dev-config/tsconfig.json new file mode 100644 index 000000000..50002ee6d --- /dev/null +++ b/packages/ocom/local-dev-config/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@cellix/config-typescript/node", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../../cellix/local-dev" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8fd2dd2e..a68aba219 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,9 @@ importers: '@cellix/local-dev': specifier: workspace:* version: link:../../packages/cellix/local-dev + '@ocom/local-dev-config': + specifier: workspace:* + version: link:../../packages/ocom/local-dev-config '@vitest/coverage-istanbul': specifier: 'catalog:' version: 4.1.6(vitest@4.1.6) @@ -421,6 +424,9 @@ importers: '@cellix/server-oauth2-mock-seedwork': specifier: workspace:* version: link:../../packages/cellix/server-oauth2-mock-seedwork + '@ocom/local-dev-config': + specifier: workspace:* + version: link:../../packages/ocom/local-dev-config dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -464,6 +470,9 @@ importers: '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.12.0) + '@ocom/local-dev-config': + specifier: workspace:* + version: link:../../packages/ocom/local-dev-config '@ocom/ui-community-route-accounts': specifier: workspace:* version: link:../../packages/ocom/ui-community-route-accounts @@ -588,6 +597,9 @@ importers: '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.12.0) + '@ocom/local-dev-config': + specifier: workspace:* + version: link:../../packages/ocom/local-dev-config '@ocom/ui-shared': specifier: workspace:* version: link:../../packages/ocom/ui-shared @@ -1640,6 +1652,25 @@ importers: specifier: 'catalog:' version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/ocom/local-dev-config: + dependencies: + '@cellix/local-dev': + specifier: workspace:* + version: link:../../cellix/local-dev + devDependencies: + '@cellix/config-typescript': + specifier: workspace:* + version: link:../../cellix/config-typescript + '@types/node': + specifier: 'catalog:' + version: 22.19.15 + rimraf: + specifier: 'catalog:' + version: 6.0.1 + typescript: + specifier: 'catalog:' + version: 6.0.3 + packages/ocom/persistence: dependencies: '@cellix/domain-seedwork': diff --git a/turbo.json b/turbo.json index 6c391b388..d1ddf8147 100644 --- a/turbo.json +++ b/turbo.json @@ -67,7 +67,7 @@ }, "test:e2e": { "description": "Runs end-to-end tests from packages that expose them", - "dependsOn": ["^build"], + "dependsOn": ["^build", "@cellix/local-dev#build", "@ocom/local-dev-config#build"], "inputs": ["$TURBO_DEFAULT$", "!coverage/**", "!target/**", "!dist/**", "!build/**", "!deploy/**"], "outputs": ["target/**", "reports/**"], "cache": false @@ -105,18 +105,19 @@ }, "dev": { "description": "Starts application packages dev servers in watch mode", - "dependsOn": ["^build"], + "dependsOn": ["^build", "@cellix/local-dev#build", "@ocom/local-dev-config#build"], "cache": false, "persistent": true }, "dev:worktree": { "description": "Starts dev servers with worktree-scoped portless hostnames for git worktree isolation", - "dependsOn": ["^build"], + "dependsOn": ["^build", "@cellix/local-dev#build", "@ocom/local-dev-config#build"], "cache": false, "persistent": true }, "azurite": { "description": "Starts the Azurite storage emulator", + "dependsOn": ["^build", "@cellix/local-dev#build", "@ocom/local-dev-config#build"], "cache": false, "persistent": true }, From 84b72852fc1e3be24f9ce4ba2ff95e92b7c9d90e Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 9 Jun 2026 09:45:14 -0400 Subject: [PATCH 22/40] made some improvements to patterns for server startup based on api, wigh finalize, and standaridzed server configs and made them object focused --- knip.json | 2 +- packages/cellix/serenity-framework/README.md | 82 ++++--- .../cellix/serenity-framework/manifest.md | 12 +- .../src/infrastructure/api-infrastructure.ts | 152 +++++++------ .../src/infrastructure/e2e-infrastructure.ts | 208 +++++++++++------- .../src/infrastructure/index.ts | 4 +- .../src/infrastructure/infrastructure.test.ts | 104 +++++---- .../serenity-framework/src/servers/index.ts | 9 +- .../mongo-memory-process-test-server.ts | 56 +++++ .../src/servers/mongo-memory-seed.ts | 26 +++ .../servers/mongo-memory-test-server.test.ts | 59 +++++ .../src/servers/mongo-memory-test-server.ts | 30 +-- .../src/servers/process-test-server.test.ts | 50 ++++- .../src/servers/process-test-server.ts | 52 ++++- .../src/servers/test-server.ts | 11 + .../community/tasks/create-community.ts | 2 +- .../src/cucumber-lifecycle-hooks.ts | 4 +- .../acceptance-api/src/infrastructure.ts | 46 +--- .../src/servers/api-graphql-test-server.ts | 50 +++++ .../acceptance-api/src/servers/index.ts | 3 + .../src/servers/mongoose-test-server.ts | 52 +++++ .../acceptance-api/src/servers/paths.ts | 7 + .../src/servers/test-mongo-server.ts | 28 +++ .../src/shared/abilities/create-community.ts | 33 ++- .../src/test-server-factories.ts | 103 --------- .../acceptance-api/src/world.ts | 20 +- .../step-definitions/header-login.steps.ts | 4 +- .../e2e-tests/src/cucumber-lifecycle-hooks.ts | 4 +- .../e2e-tests/src/infrastructure.ts | 49 +---- .../src/servers/community-ui-portal-server.ts | 23 ++ .../e2e-tests/src/servers/index.ts | 6 + .../src/servers/staff-ui-portal-server.ts | 23 ++ .../e2e-tests/src/servers/test-api-server.ts | 29 +++ .../src/servers/test-azurite-server.ts | 21 ++ .../src/servers/test-mongo-server.ts | 25 +++ .../src/servers/test-oauth2-server.ts | 17 ++ .../src/shared/environment/app-paths.ts | 1 + .../e2e-tests/src/test-server-factories.ts | 108 --------- .../ocom-verification/e2e-tests/src/world.ts | 9 +- .../verification-shared/package.json | 1 - .../src/abilities/create-community.ts | 53 ----- .../src/abilities/index.ts | 2 - 42 files changed, 934 insertions(+), 646 deletions(-) create mode 100644 packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts create mode 100644 packages/cellix/serenity-framework/src/servers/mongo-memory-seed.ts create mode 100644 packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts create mode 100644 packages/ocom-verification/acceptance-api/src/servers/api-graphql-test-server.ts create mode 100644 packages/ocom-verification/acceptance-api/src/servers/index.ts create mode 100644 packages/ocom-verification/acceptance-api/src/servers/mongoose-test-server.ts create mode 100644 packages/ocom-verification/acceptance-api/src/servers/paths.ts create mode 100644 packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts delete mode 100644 packages/ocom-verification/acceptance-api/src/test-server-factories.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/index.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts create mode 100644 packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts delete mode 100644 packages/ocom-verification/e2e-tests/src/test-server-factories.ts delete mode 100644 packages/ocom-verification/verification-shared/src/abilities/create-community.ts delete mode 100644 packages/ocom-verification/verification-shared/src/abilities/index.ts diff --git a/knip.json b/knip.json index e3de3d8c4..662302097 100644 --- a/knip.json +++ b/knip.json @@ -67,7 +67,7 @@ "project": ["src/**/*.{ts,cjs}"] }, "packages/ocom-verification/verification-shared": { - "entry": ["src/abilities/index.ts", "src/pages/index.ts", "src/test-data/index.ts"], + "entry": ["src/pages/index.ts", "src/test-data/index.ts"], "project": ["src/**/*.ts"] }, "packages/ocom-verification/acceptance-api": { diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index bf628eeeb..3067ae41b 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -32,37 +32,54 @@ import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwrig ## Server composition -Compose only the servers a suite needs by chaining `addServer` and `addUiPortal`. The framework starts them in dependency order (servers whose `dependsOn` is satisfied start in parallel), resets per-scenario state, sets up the browser when a UI portal exists, and tears everything down. Each application registers its own set — fewer or more servers — without changing the framework. +Compose only the servers a suite needs by chaining `addServer`, then `addUiPortal`, then `finalize`. The framework starts them in dependency order (servers whose `dependsOn` is satisfied start in parallel), calls `resetForScenario()` on servers that implement it, sets up the browser when a UI portal exists, and tears everything down. Each application registers its own set — fewer or more servers — without changing the framework. Once UI portal registration starts, regular server registration is closed; once finalized, no registration methods remain on the typed runtime surface. ```ts import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; -import { MongoMemoryTestServer, ProcessTestServer } from '@cellix/serenity-framework/servers'; +import { MongoMemoryProcessTestServer, ProcessTestServer, ProcessUiTestServer } from '@cellix/serenity-framework/servers'; + +const mongo = new MongoMemoryProcessTestServer({ + serverName: 'Mongo', + executable: 'pnpm', + spawnArgs: ['run', 'dev'], + cwd: '/repo/apps/server-mongodb-memory-mock', + connectionString: () => mongoConnectionString, + dbName, + portsToCloseBeforeStart: () => mongoPort, + readyMarker: 'MongoDB Memory Replica Set ready at:', + seedData, +}); + +const api = new ProcessTestServer({ + serverName: 'Api', + executable: 'pnpm', + spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], + cwd: '/repo/apps/api', + extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: mongo.getConnectionString() }), + getUrl: () => 'https://api.localhost:1355/api/graphql', + readyMarker: 'Functions:', +}); + +const community = new ProcessUiTestServer({ + serverName: 'Community portal', + executable: 'pnpm', + spawnArgs: ['run', 'dev'], + cwd: '/repo/apps/ui-community', + getUrl: () => 'https://community.localhost:1355', + readyMarker: 'ready in', +}); export const infrastructure = E2EInfrastructure .create({ // Shared across every portal context; baseURL is supplied per portal. browserContextOptions: { ignoreHTTPSErrors: true }, }) - .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { - resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - }) - .addServer('auth', () => createAuthServer()) - .addServer( - 'api', - (ctx) => - new ProcessTestServer({ - serverName: 'Api', - executable: 'pnpm', - spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], - cwd: '/repo/apps/api', - extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: ctx.server('mongo').getConnectionString() }), - getUrl: () => 'https://api.localhost:1355/api/graphql', - readyMarker: 'Functions:', - }), - { dependsOn: ['mongo'] }, - ) - .addUiPortal('community', () => createCommunityPortal()) - .addUiPortal('staff', () => createStaffPortal()); + .addServer('mongo', mongo) + .addServer('auth', createAuthServer()) + .addServer('api', api, { dependsOn: ['mongo'] }) + .addUiPortal('community', community) + .addUiPortal('staff', createStaffPortal()) + .finalize(); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); @@ -73,7 +90,7 @@ await infrastructure.stopAll(); const staffContext = await infrastructure.newPortalContext('staff'); ``` -A server factory receives a context so a dependent server (such as the API) can read a started dependency's runtime state (such as the database connection string). Each UI portal owns its browser-context recipe, so `newPortalContext(name)` scopes navigation to that portal without any portal being special-cased. The framework never imports app paths or app-specific environment helpers — pass those into the factories from the consumer package. +Register server objects directly. Dependencies that need one another receive those references through normal object construction; `dependsOn` describes startup order only. Each UI portal owns its browser-context recipe, so `newPortalContext(name)` scopes navigation to that portal without any portal being special-cased. The framework never imports app paths or app-specific environment helpers — pass those into server constructors from the consumer package. ## API acceptance infrastructure @@ -81,22 +98,21 @@ API-only acceptance suites use the smaller infrastructure manager. It composes a ```ts import { ApiInfrastructure } from '@cellix/serenity-framework/infrastructure/api'; -import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; +import { MongoMemoryProcessTestServer } from '@cellix/serenity-framework/servers'; + +const mongo = new MongoMemoryProcessTestServer({ serverName, executable, spawnArgs, cwd, connectionString, dbName, portsToCloseBeforeStart: mongoPort, readyMarker, seedData }); +const graphql = createGraphqlServer(mongo); export const infrastructure = ApiInfrastructure .create() - .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { - resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - }) - .addServer( - 'graphql', - (ctx) => createGraphqlServer(ctx.server('mongo').getConnectionString()), - { dependsOn: ['mongo'] }, - ); + .addServer('mongo', mongo) + .addServer('graphql', graphql, { dependsOn: ['mongo'] }) + .finalize(); // Suites without a database can register only their API server. const apiOnly = ApiInfrastructure.create() - .addServer('graphql', () => createGraphqlServer()); + .addServer('graphql', createGraphqlServer()) + .finalize(); ``` ## Managed worlds diff --git a/packages/cellix/serenity-framework/manifest.md b/packages/cellix/serenity-framework/manifest.md index 9dae46cf9..f7bbd0b9b 100644 --- a/packages/cellix/serenity-framework/manifest.md +++ b/packages/cellix/serenity-framework/manifest.md @@ -14,8 +14,8 @@ This package owns generic verification infrastructure only: - DOM globals (via happy-dom), CSS module declarations, asset-loader hooks, and generic React render helpers for component acceptance tests - Adapter-backed page-object base contracts - Timeout utilities -- Configurable process, Apollo GraphQL, and Mongo memory server lifecycle utilities -- API acceptance and browser E2E infrastructure managers that compose consumer-owned server factories; the E2E manager starts a freely-composed, dependency-ordered server set and the browser +- Configurable process, Apollo GraphQL, and Mongo memory server lifecycle utilities, including process-backed Mongo reset/seed support +- API acceptance and browser E2E infrastructure managers that compose consumer-owned server instances; the E2E manager starts a dependency-ordered regular server set, then UI portal servers, then the browser ## Non-goals @@ -38,14 +38,14 @@ This package owns generic verification infrastructure only: - `@cellix/serenity-framework/dom/css-modules`: package-owned CSS module declaration target - `@cellix/serenity-framework/serenity`: `TaskStep`, `SerenityCast` - `@cellix/serenity-framework/serenity/browser`: `BrowseTheWeb` -- `@cellix/serenity-framework/infrastructure/api`: API acceptance infrastructure manager with MongoDB options, optional Mongoose service management, and an API server factory -- `@cellix/serenity-framework/infrastructure/e2e`: browser E2E infrastructure manager that composes a dependency-ordered server set via chainable `addServer`/`addUiPortal` and sets up the browser -- `@cellix/serenity-framework/servers`: generic server lifecycle classes and interfaces +- `@cellix/serenity-framework/infrastructure/api`: API acceptance infrastructure manager with chainable `addServer(...).finalize()` registration and server-owned scenario reset +- `@cellix/serenity-framework/infrastructure/e2e`: browser E2E infrastructure manager that composes regular servers before UI portal servers via `addServer(...).addUiPortal(...).finalize()` and sets up the browser +- `@cellix/serenity-framework/servers`: generic server lifecycle classes and interfaces, including `UiTestServer` / `ProcessUiTestServer` for browser portal registration, process-owned fixed-port cleanup, and `MongoMemoryProcessTestServer` for process-backed Mongo startup with reset/seed behavior - `@cellix/serenity-framework/settings`: timeout helpers ## Package boundaries -The package must not import from `@ocom/*`, `@ocom-verification/*`, `apps/*`, or local OCOM path helpers. Consumers pass app-specific values through options objects, descriptors, factories, or callbacks. +The package must not import from `@ocom/*`, `@ocom-verification/*`, `apps/*`, or local OCOM path helpers. Consumers pass app-specific values through options objects, descriptors, server instances, or callbacks. ## Dependencies / relationships diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts index 4c47b4817..8f5e4e4b0 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -9,29 +9,10 @@ export interface ApiInfrastructureOptions { cleanupEnvironment?: () => Promise | void; } -/** Lookup passed to server factories so a server can read its already-started dependencies. */ -export interface ApiServerContext { - /** - * Resolve an already-started server by name. - * - * @typeParam TServer Concrete server type, for example `MongoMemoryTestServer`. - * @param name Name the dependency was registered with. - * @throws Error when the named server has not been created yet. Declare it in - * `dependsOn` so the framework starts it first. - */ - server(name: string): TServer; -} - -/** Factory that creates a server, optionally reading its dependencies from {@link ApiServerContext}. */ -export type ApiServerFactory = (context: ApiServerContext) => TestServer; - /** Options accepted when registering a server with {@link ApiInfrastructure.addServer}. */ export interface ApiServerOptions { /** Names of servers that must be started before this one. */ dependsOn?: string[]; - - /** Reset this server between scenarios, e.g. clear and reseed a database. */ - resetForScenario?: (server: TestServer) => Promise | void; } /** State exposed by {@link ApiInfrastructure}. */ @@ -42,58 +23,80 @@ export interface ApiInfrastructureState { interface ServerRegistration { name: string; - factory: ApiServerFactory; + server: TestServer; dependsOn: string[]; - resetForScenario?: (server: TestServer) => Promise | void; +} + +/** Fluent registration phase for API acceptance infrastructure. */ +export interface ApiInfrastructureServerChain { + /** + * Register a server. + * + * @param name Unique server name, used by `dependsOn`. + * @param server Test server instance that owns its own lifecycle. + * @param options Dependencies. + */ + addServer(name: string, server: TestServer, options?: ApiServerOptions): ApiInfrastructureServerChain; + + /** Freeze registration and return the runnable infrastructure. */ + finalize(): ApiInfrastructureRuntime; +} + +/** Runnable API acceptance infrastructure after registration is finalized. */ +export interface ApiInfrastructureRuntime { + /** Start the environment and all servers in dependency order. */ + ensureStarted(): Promise; + + /** Reset mutable scenario state for every running server that implements reset. */ + resetScenarioState(): Promise; + + /** Stop every created server, including partial starts, then clean up the suite environment. */ + stopAll(): Promise; + + /** Return the current infrastructure state. */ + getState(): ApiInfrastructureState; } /** * Lifecycle manager for API acceptance test suites. * - * Servers are composed fluently with {@link addServer} rather than being fixed up - * front, so each consuming suite registers only the servers it needs — a database - * here, an ORM connection there, a GraphQL server on top — and the framework owns - * startup ordering, scenario reset, and shutdown. The manager is ignorant of what - * each server is: a Mongo memory server, a SQL server, an Apollo GraphQL server, - * or anything else implementing {@link TestServer}. A suite with no database, a + * Servers are composed fluently with {@link addServer}, so each consuming suite + * registers only the server instances it needs — a database here, an ORM + * connection there, a GraphQL server on top — and the framework owns startup + * ordering, scenario reset, and shutdown. The manager is ignorant of what each + * server is: a Mongo memory server, a SQL server, an Apollo GraphQL server, or + * anything else implementing {@link TestServer}. A suite with no database, a * non-Mongo database, or no GraphQL layer simply registers a different server set. * - * Servers start in dependency waves: every server whose `dependsOn` is satisfied - * starts in parallel, then the next wave, and so on. A factory receives an - * {@link ApiServerContext} so a dependent server (for example a GraphQL server) can - * read a dependency's runtime state (for example a database connection string). + * Servers start in dependency waves: every server whose `dependsOn` is + * satisfied starts in parallel, then the next wave, and so on. Dependencies that + * need references to one another should receive those references through normal + * object construction; `dependsOn` only describes startup order. * * @example * ```ts * // With a database and a GraphQL server: + * const mongo = new MongoMemoryTestServer({ dbName, port, replSetName, seedData }); + * const graphql = createGraphqlServer(mongo); + * * ApiInfrastructure.create() - * .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { - * resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - * }) - * .addServer('graphql', (ctx) => createGraphqlServer(() => ctx.server('mongo').getUrl()), { - * dependsOn: ['mongo'], - * }); + * .addServer('mongo', mongo) + * .addServer('graphql', graphql, { dependsOn: ['mongo'] }) + * .finalize(); * * // Without a database: - * ApiInfrastructure.create().addServer('graphql', () => createGraphqlServer()); + * ApiInfrastructure.create().addServer('graphql', createGraphqlServer()).finalize(); * ``` */ -export class ApiInfrastructure { +export class ApiInfrastructure implements ApiInfrastructureServerChain, ApiInfrastructureRuntime { + private static readonly shutdownTargets = new Set(); + private readonly registrations: ServerRegistration[] = []; private readonly created = new Map(); private readonly startOrder: string[] = []; - private readonly context: ApiServerContext = { - server: (name: string): TServer => { - const server = this.created.get(name); - if (!server) { - throw new Error(`ApiInfrastructure: server '${name}' is not available — declare it in dependsOn so it starts first`); - } - return server as TServer; - }, - }; private environmentReady = false; - private shutdownHandlersRegistered = false; + private finalized = false; private constructor(private readonly options: ApiInfrastructureOptions) {} @@ -102,31 +105,42 @@ export class ApiInfrastructure { * * @param options Suite-environment setup. Defaults to an empty object. */ - static create(options: ApiInfrastructureOptions = {}): ApiInfrastructure { + static create(options: ApiInfrastructureOptions = {}): ApiInfrastructureServerChain { return new ApiInfrastructure(options); } /** * Register a server. * - * @param name Unique server name, used by `dependsOn` and {@link ApiServerContext.server}. - * @param factory Creates the server, with access to already-started dependencies. + * @param name Unique server name, used by `dependsOn`. + * @param server Test server instance that owns its own lifecycle. * @param options Dependencies and optional per-scenario reset. */ - addServer(name: string, factory: ApiServerFactory, options: ApiServerOptions = {}): this { + addServer(name: string, server: TestServer, options: ApiServerOptions = {}): ApiInfrastructureServerChain { + this.assertCanRegister('addServer'); if (this.registrations.some((registration) => registration.name === name)) { throw new Error(`ApiInfrastructure: server '${name}' is already registered`); } this.registrations.push({ name, - factory, + server, dependsOn: options.dependsOn ?? [], - ...(options.resetForScenario && { resetForScenario: options.resetForScenario }), }); return this; } + /** Freeze registration and return the runnable infrastructure. */ + finalize(): ApiInfrastructureRuntime { + this.finalized = true; + const shouldRegisterShutdownHandlers = ApiInfrastructure.shutdownTargets.size === 0; + ApiInfrastructure.shutdownTargets.add(this); + if (shouldRegisterShutdownHandlers) { + ApiInfrastructure.installProcessShutdownHandlers(); + } + return this; + } + /** Start the environment and all servers (in dependency order) if they are not already running. */ async ensureStarted(): Promise { this.assertDependenciesResolvable(); @@ -144,8 +158,8 @@ export class ApiInfrastructure { async resetScenarioState(): Promise { for (const registration of this.registrations) { const server = this.created.get(registration.name); - if (registration.resetForScenario && server?.isRunning()) { - await registration.resetForScenario(server); + if (server?.isRunning()) { + await server.resetForScenario?.(); } } } @@ -178,22 +192,21 @@ export class ApiInfrastructure { }; } - /** Register SIGINT and SIGTERM handlers that stop infrastructure before exiting. */ - registerProcessShutdownHandlers(): this { - if (this.shutdownHandlersRegistered) { - return this; - } - - this.shutdownHandlersRegistered = true; + private static installProcessShutdownHandlers(): void { const shutdown = (signal: NodeJS.Signals) => { - void this.stopAll().finally(() => { + void Promise.allSettled([...ApiInfrastructure.shutdownTargets].map((target) => target.stopAll())).finally(() => { process.exit(signal === 'SIGINT' ? 130 : 143); }); }; process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); - return this; + } + + private assertCanRegister(method: string): void { + if (this.finalized) { + throw new Error(`ApiInfrastructure: cannot call ${method} after finalize`); + } } private assertDependenciesResolvable(): void { @@ -240,11 +253,8 @@ export class ApiInfrastructure { } private async startServer(registration: ServerRegistration): Promise { - let server = this.created.get(registration.name); - if (!server) { - server = registration.factory(this.context); - this.created.set(registration.name, server); - } + const server = registration.server; + this.created.set(registration.name, server); if (!server.isRunning()) { await server.start(); diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts index d9d9e9514..0c18f385d 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts @@ -1,6 +1,6 @@ import { type Browser, type BrowserContext, type BrowserContextOptions, chromium } from 'playwright'; import { BrowseTheWeb } from '../serenity/browse-the-web.ts'; -import type { TestServer } from '../servers/test-server.ts'; +import type { TestServer, UiTestServer } from '../servers/test-server.ts'; /** State exposed by {@link E2EInfrastructure}. */ export interface E2EInfrastructureState { @@ -17,29 +17,10 @@ export interface E2EInfrastructureState { browseTheWeb: BrowseTheWeb | undefined; } -/** Lookup passed to server factories so a server can read its already-started dependencies. */ -export interface E2EServerContext { - /** - * Resolve an already-started server by name. - * - * @typeParam TServer Concrete server type, for example `MongoMemoryTestServer`. - * @param name Name the dependency was registered with. - * @throws Error when the named server has not been created yet. Declare it in - * `dependsOn` so the framework starts it first. - */ - server(name: string): TServer; -} - -/** Factory that creates a server, optionally reading its dependencies from {@link E2EServerContext}. */ -export type E2EServerFactory = (context: E2EServerContext) => TestServer; - /** Options accepted when registering a server with {@link E2EInfrastructure.addServer}. */ export interface E2EServerOptions { /** Names of servers that must be started before this one. */ dependsOn?: string[]; - - /** Reset this server between scenarios, e.g. clear and reseed a database. */ - resetForScenario?: (server: TestServer) => Promise | void; } /** Options accepted when registering a UI portal with {@link E2EInfrastructure.addUiPortal}. */ @@ -74,69 +55,118 @@ export interface E2EInfrastructureOptions { interface ServerRegistration { name: string; - factory: E2EServerFactory; + server: TestServer; dependsOn: string[]; - resetForScenario?: (server: TestServer) => Promise | void; contextOptions?: BrowserContextOptions; isUiPortal: boolean; } +/** Fluent registration phase before any UI portal has been added. */ +export interface E2EServerRegistrationChain { + /** + * Register a non-UI server. + * + * @param name Unique server name, used by `dependsOn`. + * @param server Test server instance that owns its own lifecycle. + * @param options Dependencies. + */ + addServer(name: string, server: TestServer, options?: E2EServerOptions): E2EServerRegistrationChain; + + /** + * Register the first UI portal server and move into the UI-only registration phase. + * + * @param name Stable logical portal name, such as `community` or `staff`. + * @param server UI portal server instance that owns its own lifecycle. + * @param options Dependencies and portal-scoped browser-context options. + */ + addUiPortal(name: string, server: UiTestServer, options?: E2EUiPortalOptions): E2EUiPortalRegistrationChain; + + /** Freeze registration and return the runnable infrastructure. */ + finalize(): E2EInfrastructureRuntime; +} + +/** Fluent registration phase after UI portal registration has started. */ +export interface E2EUiPortalRegistrationChain { + /** + * Register another UI portal server. + * + * @param name Stable logical portal name, such as `community` or `staff`. + * @param server UI portal server instance that owns its own lifecycle. + * @param options Dependencies and portal-scoped browser-context options. + */ + addUiPortal(name: string, server: UiTestServer, options?: E2EUiPortalOptions): E2EUiPortalRegistrationChain; + + /** Freeze registration and return the runnable infrastructure. */ + finalize(): E2EInfrastructureRuntime; +} + +/** Runnable browser E2E infrastructure after registration is finalized. */ +export interface E2EInfrastructureRuntime { + /** Open a fresh browser context scoped to a UI portal. */ + newPortalContext(name: string): Promise; + + /** Start the environment, all servers, and the browser ability. */ + ensureStarted(): Promise; + + /** Reset mutable scenario state for every running server that implements reset. */ + resetScenarioState(): Promise; + + /** Stop browser resources, every created server, and the suite environment. */ + stopAll(): Promise; + + /** Return the current infrastructure state. */ + getState(): E2EInfrastructureState; +} + /** * Lifecycle manager for browser E2E test suites. * - * Servers are composed fluently with {@link addServer} and {@link addUiPortal} - * instead of being fixed up front, so each consuming application registers only - * the servers it needs — a database here, an auth server there, one or many UI - * portals — and the framework owns startup ordering, scenario reset, browser - * setup, and shutdown. + * Servers are composed fluently with {@link addServer} and {@link addUiPortal}, + * so each consuming application registers only the server instances it needs — + * a database here, an auth server there, one or many UI portals — and the + * framework owns startup ordering, scenario reset, browser setup, and shutdown. * - * Servers start in dependency waves: every server whose `dependsOn` is satisfied - * starts in parallel, then the next wave, and so on. A factory receives an - * {@link E2EServerContext} so a dependent server (for example an API) can read a - * dependency's runtime state (for example a database connection string). The - * browser is launched only when at least one UI portal is registered, and each - * portal carries its own browser-context recipe — its `baseURL` is the portal's - * own URL — so {@link newPortalContext} opens a context for any portal without a - * caller naming the URL. The first registered portal backs the default - * `BrowseTheWeb` ability exposed on the state. + * Servers start in dependency waves: every server whose `dependsOn` is + * satisfied starts in parallel, then the next wave, and so on. Dependencies that + * need references to one another should receive those references through normal + * object construction; `dependsOn` only describes startup order. The browser is + * launched only when at least one UI portal is registered, and each portal + * carries its own browser-context recipe — its `baseURL` is the portal's own URL + * — so {@link newPortalContext} opens a context for any portal without a caller + * naming the URL. The first registered portal backs the default `BrowseTheWeb` + * ability exposed on the state. * * @example * ```ts + * const mongo = new MongoMemoryTestServer({ dbName, port, replSetName, seedData }); + * const api = createApiServer(mongo); + * * export const infrastructure = E2EInfrastructure * .create({ browserContextOptions: { ignoreHTTPSErrors: true } }) - * .addServer('mongo', () => new MongoMemoryTestServer({ dbName, port, replSetName, seedData }), { - * resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - * }) - * .addServer('auth', () => createAuthServer()) - * .addServer('api', (ctx) => createApiServer(() => ctx.server('mongo').getConnectionString()), { - * dependsOn: ['mongo'], - * }) - * .addUiPortal('community', () => createCommunityPortal()) - * .addUiPortal('staff', () => createStaffPortal()); + * .addServer('mongo', mongo) + * .addServer('auth', createAuthServer()) + * .addServer('api', api, { dependsOn: ['mongo'] }) + * .addUiPortal('community', createCommunityPortal()) + * .addUiPortal('staff', createStaffPortal()) + * .finalize(); * * // Browse any portal — baseURL is that portal's own URL: * const staffContext = await infrastructure.newPortalContext('staff'); * ``` */ -export class E2EInfrastructure { +export class E2EInfrastructure implements E2EServerRegistrationChain, E2EUiPortalRegistrationChain, E2EInfrastructureRuntime { + private static readonly shutdownTargets = new Set(); + private readonly registrations: ServerRegistration[] = []; private readonly created = new Map(); private readonly startOrder: string[] = []; - private readonly context: E2EServerContext = { - server: (name: string): TServer => { - const server = this.created.get(name); - if (!server) { - throw new Error(`E2EInfrastructure: server '${name}' is not available — declare it in dependsOn so it starts first`); - } - return server as TServer; - }, - }; private environmentReady = false; private browser: Browser | undefined; private browserContext: BrowserContext | undefined; private browseTheWeb: BrowseTheWeb | undefined; - private shutdownHandlersRegistered = false; + private finalized = false; + private uiRegistrationStarted = false; private constructor(private readonly options: E2EInfrastructureOptions) {} @@ -145,19 +175,23 @@ export class E2EInfrastructure { * * @param options Browser and suite-environment setup. Defaults to an empty object. */ - static create(options: E2EInfrastructureOptions = {}): E2EInfrastructure { + static create(options: E2EInfrastructureOptions = {}): E2EServerRegistrationChain { return new E2EInfrastructure(options); } /** * Register a server. * - * @param name Unique server name, used by `dependsOn` and {@link E2EServerContext.server}. - * @param factory Creates the server, with access to already-started dependencies. + * @param name Unique server name, used by `dependsOn`. + * @param server Test server instance that owns its own lifecycle. * @param options Dependencies and optional per-scenario reset. */ - addServer(name: string, factory: E2EServerFactory, options: E2EServerOptions = {}): this { - return this.register(name, factory, options, false); + addServer(name: string, server: TestServer, options: E2EServerOptions = {}): E2EServerRegistrationChain { + this.assertCanRegister('addServer'); + if (this.uiRegistrationStarted) { + throw new Error('E2EInfrastructure: cannot call addServer after addUiPortal'); + } + return this.register(name, server, options, false); } /** @@ -165,11 +199,24 @@ export class E2EInfrastructure { * {@link E2EInfrastructureState.uiPortalBaseUrls} and gate browser startup. * * @param name Stable logical portal name, such as `community` or `staff`. - * @param factory Creates the portal server. + * @param server UI portal server instance that owns its own lifecycle. * @param options Dependencies, per-scenario reset, and portal-scoped browser-context options. */ - addUiPortal(name: string, factory: E2EServerFactory, options: E2EUiPortalOptions = {}): this { - return this.register(name, factory, options, true); + addUiPortal(name: string, server: UiTestServer, options: E2EUiPortalOptions = {}): E2EUiPortalRegistrationChain { + this.assertCanRegister('addUiPortal'); + this.uiRegistrationStarted = true; + return this.register(name, server, options, true); + } + + /** Freeze registration and return the runnable infrastructure. */ + finalize(): E2EInfrastructureRuntime { + this.finalized = true; + const shouldRegisterShutdownHandlers = E2EInfrastructure.shutdownTargets.size === 0; + E2EInfrastructure.shutdownTargets.add(this); + if (shouldRegisterShutdownHandlers) { + E2EInfrastructure.installProcessShutdownHandlers(); + } + return this; } /** @@ -204,8 +251,8 @@ export class E2EInfrastructure { async resetScenarioState(): Promise { for (const registration of this.registrations) { const server = this.created.get(registration.name); - if (registration.resetForScenario && server?.isRunning()) { - await registration.resetForScenario(server); + if (server?.isRunning()) { + await server.resetForScenario?.(); } } } @@ -251,40 +298,38 @@ export class E2EInfrastructure { }; } - /** Register SIGINT and SIGTERM handlers that stop infrastructure before exiting. */ - registerProcessShutdownHandlers(): this { - if (this.shutdownHandlersRegistered) { - return this; - } - - this.shutdownHandlersRegistered = true; + private static installProcessShutdownHandlers(): void { const shutdown = (signal: NodeJS.Signals) => { - void this.stopAll().finally(() => { + void Promise.allSettled([...E2EInfrastructure.shutdownTargets].map((target) => target.stopAll())).finally(() => { process.exit(signal === 'SIGINT' ? 130 : 143); }); }; process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown); - return this; } - private register(name: string, factory: E2EServerFactory, options: E2EUiPortalOptions, isUiPortal: boolean): this { + private register(name: string, server: TestServer, options: E2EUiPortalOptions, isUiPortal: boolean): this { if (this.registrations.some((registration) => registration.name === name)) { throw new Error(`E2EInfrastructure: server '${name}' is already registered`); } this.registrations.push({ name, - factory, + server, dependsOn: options.dependsOn ?? [], - ...(options.resetForScenario && { resetForScenario: options.resetForScenario }), ...(options.contextOptions && { contextOptions: options.contextOptions }), isUiPortal, }); return this; } + private assertCanRegister(method: string): void { + if (this.finalized) { + throw new Error(`E2EInfrastructure: cannot call ${method} after finalize`); + } + } + private hasUiPortal(): boolean { return this.registrations.some((registration) => registration.isUiPortal); } @@ -333,11 +378,8 @@ export class E2EInfrastructure { } private async startServer(registration: ServerRegistration): Promise { - let server = this.created.get(registration.name); - if (!server) { - server = registration.factory(this.context); - this.created.set(registration.name, server); - } + const server = registration.server; + this.created.set(registration.name, server); if (!server.isRunning()) { await server.start(); diff --git a/packages/cellix/serenity-framework/src/infrastructure/index.ts b/packages/cellix/serenity-framework/src/infrastructure/index.ts index ffd562641..b3d80cec8 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/index.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/index.ts @@ -1,4 +1,4 @@ -export type { ApiInfrastructureOptions, ApiInfrastructureState, ApiServerContext, ApiServerFactory, ApiServerOptions } from './api-infrastructure.ts'; +export type { ApiInfrastructureOptions, ApiInfrastructureRuntime, ApiInfrastructureServerChain, ApiInfrastructureState, ApiServerOptions } from './api-infrastructure.ts'; export { ApiInfrastructure } from './api-infrastructure.ts'; -export type { E2EInfrastructureOptions, E2EInfrastructureState, E2EServerContext, E2EServerFactory, E2EServerOptions, E2EUiPortalOptions } from './e2e-infrastructure.ts'; +export type { E2EInfrastructureOptions, E2EInfrastructureRuntime, E2EInfrastructureState, E2EServerOptions, E2EServerRegistrationChain, E2EUiPortalOptions, E2EUiPortalRegistrationChain } from './e2e-infrastructure.ts'; export { E2EInfrastructure } from './e2e-infrastructure.ts'; diff --git a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts index 7b32b5bcf..bde792418 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/infrastructure.test.ts @@ -5,8 +5,7 @@ vi.mock('playwright', () => ({ chromium: { launch: vi.fn() }, })); -import type { MongoMemoryTestServer } from '../servers/mongo-memory-test-server.ts'; -import type { TestServer } from '../servers/test-server.ts'; +import type { MongoMemoryTestServer, TestServer, UiTestServer } from '../servers/index.ts'; import { ApiInfrastructure, E2EInfrastructure } from './index.ts'; class FakeServer implements TestServer { @@ -65,6 +64,10 @@ class FailingServer extends FakeServer { } } +class FakeUiServer extends FakeServer implements UiTestServer { + readonly uiPortal = true; +} + function mongoServer(url = 'mongodb://test'): FakeServer & MongoMemoryTestServer { return new FakeServer(url) as FakeServer & MongoMemoryTestServer; } @@ -87,34 +90,27 @@ function browserStubs() { } describe('ApiInfrastructure', () => { - it('starts servers in dependency order and exposes them by name, wiring the factory context', async () => { + it('starts server instances in dependency order and exposes them by name', async () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); - let graphqlSawMongoUrl: string | undefined; const infrastructure = ApiInfrastructure.create() - .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) - .addServer( - 'graphql', - (ctx) => { - graphqlSawMongoUrl = ctx.server('mongo').getUrl(); - return graphQL; - }, - { dependsOn: ['mongo'] }, - ); + .addServer('mongo', mongo) + .addServer('graphql', graphQL, { dependsOn: ['mongo'] }) + .finalize(); await infrastructure.ensureStarted(); - expect(graphqlSawMongoUrl).toBe('mongodb://test'); expect(Object.keys(infrastructure.getState().servers)).toEqual(['mongo', 'graphql']); expect(graphQL.getUrl()).toBe('http://127.0.0.1:4000/graphql'); expect(graphQL.startCalls).toBe(1); + expect(mongo.startCalls).toBe(1); }); it('runs a single server without a database when none is registered', async () => { const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); - const infrastructure = ApiInfrastructure.create().addServer('graphql', () => graphQL); + const infrastructure = ApiInfrastructure.create().addServer('graphql', graphQL).finalize(); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); @@ -128,8 +124,9 @@ describe('ApiInfrastructure', () => { const mongo = mongoServer(); const graphQL = new FakeServer('http://127.0.0.1:4000/graphql'); const infrastructure = ApiInfrastructure.create() - .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) - .addServer('graphql', () => graphQL, { dependsOn: ['mongo'] }); + .addServer('mongo', mongo) + .addServer('graphql', graphQL, { dependsOn: ['mongo'] }) + .finalize(); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); @@ -140,13 +137,15 @@ describe('ApiInfrastructure', () => { }); it('rejects duplicate server names', () => { - const infrastructure = ApiInfrastructure.create().addServer('graphql', () => new FakeServer('http://127.0.0.1:4000/graphql')); + const infrastructure = ApiInfrastructure.create().addServer('graphql', new FakeServer('http://127.0.0.1:4000/graphql')); - expect(() => infrastructure.addServer('graphql', () => new FakeServer('http://127.0.0.1:4001/graphql'))).toThrow(/already registered/); + expect(() => infrastructure.addServer('graphql', new FakeServer('http://127.0.0.1:4001/graphql'))).toThrow(/already registered/); }); it('rejects an unknown dependency', async () => { - const infrastructure = ApiInfrastructure.create().addServer('graphql', () => new FakeServer('http://127.0.0.1:4000/graphql'), { dependsOn: ['mongo'] }); + const infrastructure = ApiInfrastructure.create() + .addServer('graphql', new FakeServer('http://127.0.0.1:4000/graphql'), { dependsOn: ['mongo'] }) + .finalize(); await expect(infrastructure.ensureStarted()).rejects.toThrow(/unknown server 'mongo'/); }); @@ -158,9 +157,7 @@ describe('ApiInfrastructure', () => { }); const slow = new DeferredServer('http://slow.test', startGate); const failing = new FailingServer('http://failing.test'); - const infrastructure = ApiInfrastructure.create() - .addServer('slow', () => slow) - .addServer('failing', () => failing); + const infrastructure = ApiInfrastructure.create().addServer('slow', slow).addServer('failing', failing).finalize(); const starting = expect(infrastructure.ensureStarted()).rejects.toThrow('startup failed'); await vi.waitFor(() => expect(failing.startCalls).toBe(1)); @@ -172,39 +169,37 @@ describe('ApiInfrastructure', () => { expect(slow.isRunning()).toBe(false); expect(infrastructure.getState().servers).toEqual({}); }); + + it('rejects registration after finalize at runtime', () => { + const infrastructure = ApiInfrastructure.create().addServer('graphql', new FakeServer('http://127.0.0.1:4000/graphql')).finalize() as ApiInfrastructure; + + expect(() => infrastructure.addServer('late', new FakeServer('http://127.0.0.1:4001/graphql'))).toThrow(/cannot call addServer after finalize/); + }); }); describe('E2EInfrastructure', () => { - it('starts servers in dependency order, wires the factory context, and exposes all portal URLs', async () => { + it('starts server instances in dependency order and exposes all portal URLs', async () => { const mongo = mongoServer(); const azurite = new FakeServer('http://127.0.0.1:10000'); const auth = new FakeServer('https://auth.test'); const api = new FakeServer('https://api.test/api/graphql'); - const community = new FakeServer('https://community.test'); - const staff = new FakeServer('https://staff.test'); + const community = new FakeUiServer('https://community.test'); + const staff = new FakeUiServer('https://staff.test'); const { browser } = browserStubs(); vi.mocked(chromium.launch).mockResolvedValue(browser); - let apiSawMongoUrl: string | undefined; const infrastructure = E2EInfrastructure.create() - .addServer('mongo', () => mongo, { resetForScenario: (server) => (server as FakeServer).resetForScenario() }) - .addServer('azurite', () => azurite) - .addServer('auth', () => auth) - .addServer( - 'api', - (ctx) => { - apiSawMongoUrl = ctx.server('mongo').getUrl(); - return api; - }, - { dependsOn: ['mongo'] }, - ) - .addUiPortal('community', () => community) - .addUiPortal('staff', () => staff); + .addServer('mongo', mongo) + .addServer('azurite', azurite) + .addServer('auth', auth) + .addServer('api', api, { dependsOn: ['mongo'] }) + .addUiPortal('community', community) + .addUiPortal('staff', staff) + .finalize(); await infrastructure.ensureStarted(); await infrastructure.resetScenarioState(); - expect(apiSawMongoUrl).toBe('mongodb://test'); expect(mongo.resetCalls).toBe(1); expect(infrastructure.getState().uiPortalBaseUrls).toEqual({ community: 'https://community.test', @@ -220,16 +215,31 @@ describe('E2EInfrastructure', () => { // Deliberately a leaner set than the suite above: no Azurite server. const infrastructure = E2EInfrastructure.create() - .addServer('mongo', () => mongoServer()) - .addServer('auth', () => new FakeServer('https://auth.test')) - .addServer('api', () => new FakeServer('https://api.test/api/graphql'), { dependsOn: ['mongo'] }) - .addUiPortal('community', () => new FakeServer('https://community.test')); + .addServer('mongo', mongoServer()) + .addServer('auth', new FakeServer('https://auth.test')) + .addServer('api', new FakeServer('https://api.test/api/graphql'), { dependsOn: ['mongo'] }) + .addUiPortal('community', new FakeUiServer('https://community.test')) + .finalize(); await infrastructure.ensureStarted(); expect(infrastructure.getState().browseTheWeb).toBeDefined(); }); + it('rejects regular server registration after UI portal registration has started', () => { + const infrastructure = E2EInfrastructure.create().addUiPortal('community', new FakeUiServer('https://community.test')) as E2EInfrastructure; + + expect(() => infrastructure.addServer('api', new FakeServer('https://api.test'))).toThrow(/cannot call addServer after addUiPortal/); + }); + + it('rejects registration after finalize at runtime', () => { + const infrastructure = E2EInfrastructure.create().addUiPortal('community', new FakeUiServer('https://community.test')).finalize() as E2EInfrastructure; + + expect(() => infrastructure.addUiPortal('staff', new FakeUiServer('https://staff.test'))).toThrow(/cannot call addUiPortal after finalize/); + }); +}); + +describe('E2EInfrastructure startup failures', () => { it('waits for a failed startup wave to settle and stops every created server', async () => { let releaseStart: () => void = () => undefined; const startGate = new Promise((resolve) => { @@ -237,9 +247,7 @@ describe('E2EInfrastructure', () => { }); const slow = new DeferredServer('http://slow.test', startGate); const failing = new FailingServer('http://failing.test'); - const infrastructure = E2EInfrastructure.create() - .addServer('slow', () => slow) - .addServer('failing', () => failing); + const infrastructure = E2EInfrastructure.create().addServer('slow', slow).addServer('failing', failing).finalize(); const starting = expect(infrastructure.ensureStarted()).rejects.toThrow('startup failed'); await vi.waitFor(() => expect(failing.startCalls).toBe(1)); diff --git a/packages/cellix/serenity-framework/src/servers/index.ts b/packages/cellix/serenity-framework/src/servers/index.ts index 995ce4500..c7021b112 100644 --- a/packages/cellix/serenity-framework/src/servers/index.ts +++ b/packages/cellix/serenity-framework/src/servers/index.ts @@ -1,8 +1,11 @@ export type { ApolloGraphQLTestServerOptions } from './apollo-graphql-test-server.ts'; export { ApolloGraphQLTestServer } from './apollo-graphql-test-server.ts'; -export type { MongoMemorySeedContext, MongoMemorySeedDataFunction, MongoMemoryTestServerOptions } from './mongo-memory-test-server.ts'; +export type { MongoMemoryProcessTestServerOptions } from './mongo-memory-process-test-server.ts'; +export { MongoMemoryProcessTestServer } from './mongo-memory-process-test-server.ts'; +export type { MongoMemorySeedContext, MongoMemorySeedDataFunction } from './mongo-memory-seed.ts'; +export type { MongoMemoryTestServerOptions } from './mongo-memory-test-server.ts'; export { MongoMemoryTestServer } from './mongo-memory-test-server.ts'; export { createSpawnEnvironment } from './process-environment.ts'; export type { ProcessHealthProbe, ProcessTestServerOptions } from './process-test-server.ts'; -export { ProcessTestServer } from './process-test-server.ts'; -export type { SeedDataFunction, TestServer } from './test-server.ts'; +export { ProcessTestServer, ProcessUiTestServer } from './process-test-server.ts'; +export type { SeedDataFunction, TestServer, UiTestServer } from './test-server.ts'; diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts new file mode 100644 index 000000000..7aefb36b5 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts @@ -0,0 +1,56 @@ +import { clearMongoMemoryDatabase, type MongoMemorySeedDataFunction } from './mongo-memory-seed.ts'; +import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; + +/** Options used by {@link MongoMemoryProcessTestServer}. */ +export interface MongoMemoryProcessTestServerOptions extends Omit { + /** Database name. */ + dbName: string; + + /** MongoDB connection string exposed by the process. */ + connectionString: string | (() => string); + + /** Optional seed function called after startup and reset. */ + seedData?: MongoMemorySeedDataFunction; +} + +/** + * Process-backed MongoDB memory server with built-in scenario reset. + * + * Consumers own the command that starts MongoDB, while this server owns the + * generic MongoDB contract: expose the connection string, clear collections, + * and rerun optional seed data on startup and scenario reset. + */ +export class MongoMemoryProcessTestServer extends ProcessTestServer { + constructor(private readonly mongoOptions: MongoMemoryProcessTestServerOptions) { + super({ + ...mongoOptions, + getUrl: () => resolveValue(mongoOptions.connectionString), + }); + } + + /** Return the MongoDB connection string. */ + getConnectionString(): string { + return this.getUrl(); + } + + /** Start the process and seed the database. */ + override async start(): Promise { + await super.start(); + await this.resetForScenario(); + } + + /** + * Clear all collections and re-run seed data. + * + * @param seedData Optional seed override for this reset. + */ + async resetForScenario(seedData?: MongoMemorySeedDataFunction): Promise { + const context = { connectionString: this.getConnectionString(), dbName: this.mongoOptions.dbName }; + await clearMongoMemoryDatabase(context); + await (seedData ?? this.mongoOptions.seedData)?.(context); + } +} + +function resolveValue(value: T | (() => T)): T { + return typeof value === 'function' ? (value as () => T)() : value; +} diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-seed.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-seed.ts new file mode 100644 index 000000000..691f14403 --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-seed.ts @@ -0,0 +1,26 @@ +import { MongoClient } from 'mongodb'; +import type { SeedDataFunction } from './test-server.ts'; + +/** Context supplied to Mongo seed functions. */ +export interface MongoMemorySeedContext { + /** MongoDB connection string. */ + connectionString: string; + + /** Database name used by the test server. */ + dbName: string; +} + +/** Seed function used by Mongo memory test servers. */ +export type MongoMemorySeedDataFunction = SeedDataFunction; + +export async function clearMongoMemoryDatabase(context: MongoMemorySeedContext): Promise { + const client = new MongoClient(context.connectionString); + try { + await client.connect(); + const db = client.db(context.dbName); + const collections = await db.listCollections({}, { nameOnly: true }).toArray(); + await Promise.all(collections.map((collection) => db.collection(collection.name).deleteMany({}))); + } finally { + await client.close(); + } +} diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts new file mode 100644 index 000000000..f33e9da6f --- /dev/null +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest'; + +const mongo = vi.hoisted(() => { + const deleteMany = vi.fn(async () => undefined); + const close = vi.fn(async () => undefined); + const connect = vi.fn(async () => undefined); + const toArray = vi.fn(async () => [{ name: 'users' }, { name: 'communities' }]); + const collection = vi.fn(() => ({ deleteMany })); + const db = vi.fn(() => ({ + collection, + listCollections: () => ({ toArray }), + })); + // biome-ignore lint:useArrowFunction - MongoClient is called with `new`. + const MongoClient = vi.fn(function () { + return { close, connect, db }; + }); + + return { close, collection, connect, db, deleteMany, MongoClient, toArray }; +}); + +vi.mock('mongodb', () => ({ + MongoClient: mongo.MongoClient, +})); + +import { MongoMemoryProcessTestServer } from './mongo-memory-process-test-server.ts'; + +describe('MongoMemoryProcessTestServer', () => { + it('clears collections and reruns seed data on startup and scenario reset', async () => { + const seedData = vi.fn(async () => undefined); + const server = new MongoMemoryProcessTestServer({ + connectionString: 'mongodb://127.0.0.1:50000/owner-community?replicaSet=globaldb', + cwd: process.cwd(), + dbName: 'owner-community', + executable: process.execPath, + probe: false, + readyMarker: 'READY', + seedData, + serverName: 'mongo process', + shutdownTimeoutMs: 500, + spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], + }); + + try { + await server.start(); + await server.resetForScenario(); + } finally { + await server.stop(); + } + + expect(seedData).toHaveBeenCalledTimes(2); + expect(seedData).toHaveBeenCalledWith({ + connectionString: 'mongodb://127.0.0.1:50000/owner-community?replicaSet=globaldb', + dbName: 'owner-community', + }); + expect(mongo.deleteMany).toHaveBeenCalledTimes(4); + expect(mongo.collection).toHaveBeenCalledWith('users'); + expect(mongo.collection).toHaveBeenCalledWith('communities'); + }); +}); diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts index f75cae48f..0db8c634b 100644 --- a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.ts @@ -1,18 +1,6 @@ import { type MongoMemoryReplicaSetConfig, type MongoMemoryReplicaSetDisposer, startMongoMemoryReplicaSet } from '@cellix/server-mongodb-memory-mock-seedwork'; -import { MongoClient } from 'mongodb'; -import type { SeedDataFunction, TestServer } from './test-server.ts'; - -/** Context supplied to Mongo seed functions. */ -export interface MongoMemorySeedContext { - /** MongoDB connection string. */ - connectionString: string; - - /** Database name used by the test server. */ - dbName: string; -} - -/** Seed function used by {@link MongoMemoryTestServer}. */ -export type MongoMemorySeedDataFunction = SeedDataFunction; +import { clearMongoMemoryDatabase, type MongoMemorySeedDataFunction } from './mongo-memory-seed.ts'; +import type { TestServer } from './test-server.ts'; /** Options used by {@link MongoMemoryTestServer}. */ export interface MongoMemoryTestServerOptions { @@ -85,7 +73,7 @@ export class MongoMemoryTestServer implements TestServer { throw new Error('MongoMemoryTestServer not started'); } - await clearDatabase({ connectionString: this.connectionString, dbName: this.options.dbName }); + await clearMongoMemoryDatabase({ connectionString: this.connectionString, dbName: this.options.dbName }); await this.seed(seedData); } @@ -108,15 +96,3 @@ export class MongoMemoryTestServer implements TestServer { await seedData?.({ connectionString: this.connectionString, dbName: this.options.dbName }); } } - -async function clearDatabase(context: MongoMemorySeedContext): Promise { - const client = new MongoClient(context.connectionString); - try { - await client.connect(); - const db = client.db(context.dbName); - const collections = await db.listCollections({}, { nameOnly: true }).toArray(); - await Promise.all(collections.map((collection) => db.collection(collection.name).deleteMany({}))); - } finally { - await client.close(); - } -} diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts index 51f05e970..2533e4005 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts @@ -1,4 +1,17 @@ -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const childProcessMock = vi.hoisted(() => ({ + execFileSync: vi.fn(), +})); + +vi.mock('node:child_process', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + execFileSync: childProcessMock.execFileSync, + }; +}); + import { ProcessTestServer } from './index.ts'; async function waitUntil(predicate: () => boolean, timeoutMs = 2_000): Promise { @@ -12,6 +25,10 @@ async function waitUntil(predicate: () => boolean, timeoutMs = 2_000): Promise { + beforeEach(() => { + childProcessMock.execFileSync.mockReset(); + }); + it('starts a process and trusts the ready marker when probing is disabled', async () => { const server = new ProcessTestServer({ serverName: 'marker-only server', @@ -50,4 +67,35 @@ describe('ProcessTestServer', () => { expect(server.isRunning()).toBe(false); }); + + it('closes configured ports before checking whether the server is already running', async () => { + childProcessMock.execFileSync.mockReturnValue('123\n456\n'); + const kill = vi.spyOn(process, 'kill').mockImplementation(() => true); + const isAlreadyRunning = vi.fn(async () => true); + const server = new ProcessTestServer({ + serverName: 'fixed-port server', + executable: process.execPath, + spawnArgs: ['-e', "console.log('READY')"], + cwd: process.cwd(), + readyMarker: 'READY', + getUrl: () => 'http://unused.test', + isAlreadyRunning, + portsToCloseBeforeStart: () => 27_017, + probe: false, + }); + + await server.start(); + + try { + expect(childProcessMock.execFileSync).toHaveBeenCalledWith('lsof', ['-ti', 'tcp:27017'], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + expect(kill).toHaveBeenCalledWith(123, 'SIGTERM'); + expect(kill).toHaveBeenCalledWith(456, 'SIGTERM'); + expect(childProcessMock.execFileSync.mock.invocationCallOrder[0] ?? 0).toBeLessThan(isAlreadyRunning.mock.invocationCallOrder[0] ?? 0); + } finally { + kill.mockRestore(); + } + }); }); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.ts index 6491e0732..272f42a6e 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.ts @@ -1,6 +1,6 @@ -import { type ChildProcess, spawn } from 'node:child_process'; +import { type ChildProcess, execFileSync, spawn } from 'node:child_process'; import { createSpawnEnvironment } from './process-environment.ts'; -import type { TestServer } from './test-server.ts'; +import type { TestServer, UiTestServer } from './test-server.ts'; /** Configuration for health probes used by {@link ProcessTestServer}. */ export interface ProcessHealthProbe { @@ -55,6 +55,9 @@ export interface ProcessTestServerOptions { /** Return true when the server is already reachable before spawning. */ isAlreadyRunning?: () => Promise; + /** Ports to clear before spawning, useful for fixed-port local dependencies. */ + portsToCloseBeforeStart?: number | number[] | (() => number | number[]); + /** Treat an early process exit as an existing reusable server. */ isReusableExit?: (stderrOutput: string) => boolean; } @@ -83,6 +86,8 @@ export class ProcessTestServer implements TestServer { return; } + this.closePortsBeforeStart(); + if (await this.isAlreadyRunning()) { return; } @@ -297,7 +302,50 @@ export class ProcessTestServer implements TestServer { childProcess.kill(signal); } + private closePortsBeforeStart(): void { + const ports = this.value(this.options.portsToCloseBeforeStart); + for (const port of Array.isArray(ports) ? ports : ports === undefined ? [] : [ports]) { + this.closeProcessesListeningOnPort(port); + } + } + + private closeProcessesListeningOnPort(port: number): void { + if (process.platform === 'win32') { + return; + } + + let output = ''; + try { + output = execFileSync('lsof', ['-ti', `tcp:${port}`], { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + } catch { + return; + } + + const pids = output + .split('\n') + .map((pid) => Number.parseInt(pid, 10)) + .filter((pid) => Number.isFinite(pid)); + + for (const pid of pids) { + try { + process.kill(pid, 'SIGTERM'); + } catch { + /* Process already exited. */ + } + } + } + private value(value: T | (() => T) | undefined): T | undefined { return typeof value === 'function' ? (value as () => T)() : value; } } + +/** + * Child-process server for browser UI portals. + */ +export class ProcessUiTestServer extends ProcessTestServer implements UiTestServer { + readonly uiPortal = true; +} diff --git a/packages/cellix/serenity-framework/src/servers/test-server.ts b/packages/cellix/serenity-framework/src/servers/test-server.ts index 1d337d2d0..dd0fbf307 100644 --- a/packages/cellix/serenity-framework/src/servers/test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/test-server.ts @@ -13,6 +13,17 @@ export interface TestServer { /** Return the URL exposed by the server. */ getUrl(): string; + + /** Reset mutable state between scenarios, when this server owns any. */ + resetForScenario?(): Promise | void; +} + +/** + * Contract for browser UI portal test servers. + */ +export interface UiTestServer extends TestServer { + /** Marker that distinguishes UI portal servers from generic test servers. */ + readonly uiPortal: true; } /** diff --git a/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts b/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts index 118c7fbee..8acc16a6d 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/community/tasks/create-community.ts @@ -1,5 +1,5 @@ -import { CreateCommunity as CreateCommunityAbility } from '@ocom-verification/verification-shared/abilities'; import { type Actor, notes, Task } from '@serenity-js/core'; +import { CreateCommunity as CreateCommunityAbility } from '../../../shared/abilities/create-community.ts'; import type { CommunityDetails, CommunityNotes } from '../notes/community-notes.ts'; export class CreateCommunity extends Task { diff --git a/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts index 66f7c3462..8def59785 100644 --- a/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/acceptance-api/src/cucumber-lifecycle-hooks.ts @@ -2,7 +2,7 @@ import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber import { getTimeout } from '@cellix/serenity-framework/settings'; import type { IWorld } from '@cucumber/cucumber'; import { isAgent } from 'std-env'; -import * as infra from './infrastructure.ts'; +import { infrastructure } from './infrastructure.ts'; import type { CellixApiWorld } from './world.ts'; let printedSuiteHeader = false; @@ -23,6 +23,6 @@ export function registerLifecycleHooks(): void { after: async (world) => { await world.cleanup(); }, - afterAll: () => infra.stopAll(), + afterAll: () => infrastructure.stopAll(), }); } diff --git a/packages/ocom-verification/acceptance-api/src/infrastructure.ts b/packages/ocom-verification/acceptance-api/src/infrastructure.ts index d16350f87..2da34d2a2 100644 --- a/packages/ocom-verification/acceptance-api/src/infrastructure.ts +++ b/packages/ocom-verification/acceptance-api/src/infrastructure.ts @@ -1,42 +1,8 @@ import { ApiInfrastructure } from '@cellix/serenity-framework/infrastructure/api'; -import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; -import { ServiceMongoose } from '@ocom/service-mongoose'; -import { getMongoPort } from '@ocom-verification/verification-shared/environment'; -import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; -import { ApiGraphQLTestServer, MongooseTestServer } from './test-server-factories.ts'; +import { apiGraphQLTestServer, mongooseTestServer, testMongoServer } from './servers/index.ts'; -const apiDbName = 'owner-community'; - -const infrastructure = ApiInfrastructure.create() - .addServer('mongo', () => new MongoMemoryTestServer({ dbName: apiDbName, port: getMongoPort(), replSetName: 'globaldb', seedData: seedDatabase }), { - resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - }) - .addServer('mongoose', (ctx) => new MongooseTestServer(() => new ServiceMongoose(ctx.server('mongo').getConnectionString(), { autoCreate: true, autoIndex: true, dbName: apiDbName })), { - dependsOn: ['mongo'], - }) - .addServer('graphql', (ctx) => new ApiGraphQLTestServer(() => ctx.server('mongoose').getService()), { dependsOn: ['mongoose'] }); - -interface ApiAcceptanceState { - graphqlUrl: string | undefined; -} - -export function getState(): ApiAcceptanceState { - // biome-ignore lint:useLiteralKeys - servers is an index-signature record; bracket access required by noPropertyAccessFromIndexSignature - const graphqlServer = infrastructure.getState().servers['graphql']; - return { - graphqlUrl: graphqlServer?.isRunning() ? graphqlServer.getUrl() : undefined, - }; -} - -export async function stopAll(): Promise { - await infrastructure.stopAll(); -} - -export async function ensureApiServers(): Promise { - infrastructure.registerProcessShutdownHandlers(); - await infrastructure.ensureStarted(); -} - -export async function resetMongoForScenario(): Promise { - await infrastructure.resetScenarioState(); -} +export const infrastructure = ApiInfrastructure.create() + .addServer('mongo', testMongoServer) + .addServer('mongoose', mongooseTestServer, { dependsOn: ['mongo'] }) + .addServer('graphql', apiGraphQLTestServer, { dependsOn: ['mongoose'] }) + .finalize(); diff --git a/packages/ocom-verification/acceptance-api/src/servers/api-graphql-test-server.ts b/packages/ocom-verification/acceptance-api/src/servers/api-graphql-test-server.ts new file mode 100644 index 000000000..5c3ce3677 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/servers/api-graphql-test-server.ts @@ -0,0 +1,50 @@ +import { ApolloGraphQLTestServer, type TestServer } from '@cellix/serenity-framework/servers'; +import type { ApplicationServices } from '@ocom/application-services'; +import { combinedSchema } from '@ocom/graphql'; +import depthLimit from 'graphql-depth-limit'; +import { applyMiddleware } from 'graphql-middleware'; +import { createMockApplicationServicesFactory } from '../mock-application-services.ts'; +import { mongooseTestServer } from './mongoose-test-server.ts'; + +/** + * {@link TestServer} that owns the Apollo GraphQL server lifecycle for the + * acceptance suite. + */ +class ApiGraphQLTestServer implements TestServer { + private readonly server: ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>; + private applicationServicesFactory: ReturnType | undefined; + + constructor() { + this.server = new ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>({ + schema: applyMiddleware(combinedSchema), + validationRules: [depthLimit(10)], + context: async ({ req }) => { + this.applicationServicesFactory ??= createMockApplicationServicesFactory(mongooseTestServer.getService()); + const applicationServices = await this.applicationServicesFactory.forRequest(req.headers.authorization ?? undefined); + if (!applicationServices) { + throw new Error('ApplicationServicesFactory required for test server'); + } + return { applicationServices }; + }, + }); + } + + start(): Promise { + return this.server.start(); + } + + async stop(): Promise { + await this.server.stop(); + this.applicationServicesFactory = undefined; + } + + isRunning(): boolean { + return this.server.isRunning(); + } + + getUrl(): string { + return this.server.getUrl(); + } +} + +export const apiGraphQLTestServer = new ApiGraphQLTestServer(); diff --git a/packages/ocom-verification/acceptance-api/src/servers/index.ts b/packages/ocom-verification/acceptance-api/src/servers/index.ts new file mode 100644 index 000000000..aef719f17 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/servers/index.ts @@ -0,0 +1,3 @@ +export { apiGraphQLTestServer } from './api-graphql-test-server.ts'; +export { mongooseTestServer } from './mongoose-test-server.ts'; +export { testMongoServer } from './test-mongo-server.ts'; diff --git a/packages/ocom-verification/acceptance-api/src/servers/mongoose-test-server.ts b/packages/ocom-verification/acceptance-api/src/servers/mongoose-test-server.ts new file mode 100644 index 000000000..8cf25c396 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/servers/mongoose-test-server.ts @@ -0,0 +1,52 @@ +import type { TestServer } from '@cellix/serenity-framework/servers'; +import { ServiceMongoose } from '@ocom/service-mongoose'; +import { mongoDbName, testMongoServer } from './test-mongo-server.ts'; + +/** + * {@link TestServer} adapter that owns the Mongoose connection lifecycle for the + * acceptance suite. + */ +class MongooseTestServer implements TestServer { + private serviceInternal: ServiceMongoose | undefined; + + async start(): Promise { + const service = new ServiceMongoose(testMongoServer.getConnectionString(), { autoCreate: true, autoIndex: true, dbName: mongoDbName }); + await service.startUp(); + // Clear any models registered on a previous connection so schemas re-register cleanly. + const { connection } = service.service; + for (const modelName of Object.keys(connection.models)) { + try { + connection.deleteModel(modelName); + } catch { + /* already deleted */ + } + } + this.serviceInternal = service; + } + + async stop(): Promise { + if (this.serviceInternal) { + await this.serviceInternal.shutDown(); + this.serviceInternal = undefined; + } + } + + isRunning(): boolean { + return this.serviceInternal !== undefined; + } + + /** Not a network server; no URL is exposed. */ + getUrl(): string { + return ''; + } + + /** The started Mongoose service. Throws if accessed before {@link start}. */ + getService(): ServiceMongoose { + if (!this.serviceInternal) { + throw new Error('MongooseTestServer not started'); + } + return this.serviceInternal; + } +} + +export const mongooseTestServer = new MongooseTestServer(); diff --git a/packages/ocom-verification/acceptance-api/src/servers/paths.ts b/packages/ocom-verification/acceptance-api/src/servers/paths.ts new file mode 100644 index 000000000..030a4cad4 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/servers/paths.ts @@ -0,0 +1,7 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const currentDir = dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = resolve(currentDir, '../../../../..'); + +export const mongodbMemoryMockDir = resolve(workspaceRoot, 'apps/server-mongodb-memory-mock'); diff --git a/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts b/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts new file mode 100644 index 000000000..dea415b23 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts @@ -0,0 +1,28 @@ +import { MongoMemoryProcessTestServer } from '@cellix/serenity-framework/servers'; +import { getMongoPort } from '@ocom-verification/verification-shared/environment'; +import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; +import { mongodbMemoryMockDir } from './paths.ts'; + +export const mongoDbName = 'owner-community'; +const mongoReplSetName = 'globaldb'; + +export const testMongoServer = new MongoMemoryProcessTestServer({ + connectionString: mongoConnectionString, + cwd: mongodbMemoryMockDir, + dbName: mongoDbName, + executable: 'pnpm', + portsToCloseBeforeStart: getMongoPort, + probe: false, + readyMarker: 'MongoDB Memory Replica Set ready at:', + seedData: seedDatabase, + serverName: 'TestMongoMemoryServer', + spawnArgs: () => ['run', getDevScript()], +}); + +function getDevScript(): 'dev' | 'dev:worktree' { + return process.env['WORKTREE_NAME'] ? 'dev:worktree' : 'dev'; +} + +function mongoConnectionString(): string { + return `mongodb://127.0.0.1:${getMongoPort()}/${mongoDbName}?replicaSet=${mongoReplSetName}`; +} diff --git a/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts b/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts index 840208333..3881c69d1 100644 --- a/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts +++ b/packages/ocom-verification/acceptance-api/src/shared/abilities/create-community.ts @@ -1,7 +1,38 @@ import { GraphQLClient } from '@cellix/serenity-framework/clients/graphql'; -import { CreateCommunity } from '@ocom-verification/verification-shared/abilities'; +import { Ability, type Actor } from '@serenity-js/core'; import { COMMUNITY_CREATE_MUTATION, GET_COMMUNITY_QUERY } from '../graphql/community-operations.ts'; +/** Community details accepted by API verification flows. */ +interface CreateCommunityDetails { + name: string; +} + +/** Result returned by the API community creation flow. */ +interface CreateCommunityResult { + id?: string; + name: string; +} + +/** Handler that performs community creation for an API actor. */ +type CreateCommunityHandler = (actor: Actor, details: CreateCommunityDetails) => Promise; + +/** + * Serenity ability that creates communities through the API verification server. + */ +export class CreateCommunity extends Ability { + constructor(private readonly handler: CreateCommunityHandler) { + super(); + } + + static using(handler: CreateCommunityHandler): CreateCommunity { + return new CreateCommunity(handler); + } + + async performAs(actor: Actor, details: CreateCommunityDetails): Promise { + return await this.handler(actor, details); + } +} + export function createCommunityAbility(): CreateCommunity { return CreateCommunity.using(async (actor, details) => { const graphql = GraphQLClient.as(actor); diff --git a/packages/ocom-verification/acceptance-api/src/test-server-factories.ts b/packages/ocom-verification/acceptance-api/src/test-server-factories.ts deleted file mode 100644 index cb7526acc..000000000 --- a/packages/ocom-verification/acceptance-api/src/test-server-factories.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ApolloGraphQLTestServer, type TestServer } from '@cellix/serenity-framework/servers'; -import type { ApplicationServices } from '@ocom/application-services'; -import { combinedSchema } from '@ocom/graphql'; -import type { ServiceMongoose } from '@ocom/service-mongoose'; -import depthLimit from 'graphql-depth-limit'; -import { applyMiddleware } from 'graphql-middleware'; -import { createMockApplicationServicesFactory } from './mock-application-services.ts'; - -/** - * {@link TestServer} adapter that owns the Mongoose connection lifecycle for the - * acceptance suite. The framework is database-ignorant, so connecting, clearing - * registered models, and disconnecting all live here. Exposes the started - * {@link ServiceMongoose} to dependent servers (e.g. the GraphQL server) via - * {@link getService}. - */ -export class MongooseTestServer implements TestServer { - private serviceInternal: ServiceMongoose | undefined; - - constructor(private readonly createService: () => ServiceMongoose) {} - - async start(): Promise { - const service = this.createService(); - await service.startUp(); - // Clear any models registered on a previous connection so schemas re-register cleanly. - const { connection } = service.service; - for (const modelName of Object.keys(connection.models)) { - try { - connection.deleteModel(modelName); - } catch { - /* already deleted */ - } - } - this.serviceInternal = service; - } - - async stop(): Promise { - if (this.serviceInternal) { - await this.serviceInternal.shutDown(); - this.serviceInternal = undefined; - } - } - - isRunning(): boolean { - return this.serviceInternal !== undefined; - } - - /** Not a network server; no URL is exposed. */ - getUrl(): string { - return ''; - } - - /** The started Mongoose service. Throws if accessed before {@link start}. */ - getService(): ServiceMongoose { - if (!this.serviceInternal) { - throw new Error('MongooseTestServer not started'); - } - return this.serviceInternal; - } -} - -/** - * {@link TestServer} that owns the Apollo GraphQL server lifecycle for the - * acceptance suite. It wires the app's schema and permissions middleware and - * builds a request-scoped mock application-services factory lazily on the first - * request, caching it for the lifetime of the running server. The cache is - * instance-scoped, so it is discarded when the server stops. - */ -export class ApiGraphQLTestServer implements TestServer { - private readonly server: ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>; - private applicationServicesFactory: ReturnType | undefined; - - constructor(getMongooseService: () => ServiceMongoose) { - this.server = new ApolloGraphQLTestServer<{ applicationServices: ApplicationServices }>({ - schema: applyMiddleware(combinedSchema), - validationRules: [depthLimit(10)], - context: async ({ req }) => { - this.applicationServicesFactory ??= createMockApplicationServicesFactory(getMongooseService()); - const applicationServices = await this.applicationServicesFactory.forRequest(req.headers.authorization ?? undefined); - if (!applicationServices) { - throw new Error('ApplicationServicesFactory required for test server'); - } - return { applicationServices }; - }, - }); - } - - start(): Promise { - return this.server.start(); - } - - async stop(): Promise { - await this.server.stop(); - this.applicationServicesFactory = undefined; - } - - isRunning(): boolean { - return this.server.isRunning(); - } - - getUrl(): string { - return this.server.getUrl(); - } -} diff --git a/packages/ocom-verification/acceptance-api/src/world.ts b/packages/ocom-verification/acceptance-api/src/world.ts index f1fed41d3..a19acf271 100644 --- a/packages/ocom-verification/acceptance-api/src/world.ts +++ b/packages/ocom-verification/acceptance-api/src/world.ts @@ -1,29 +1,31 @@ import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import type { ApiInfrastructureState } from '@cellix/serenity-framework/infrastructure/api'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; import { registerLifecycleHooks } from './cucumber-lifecycle-hooks.ts'; -import * as infra from './infrastructure.ts'; +import { infrastructure } from './infrastructure.ts'; import { createCommunityAbility } from './shared/abilities/create-community.ts'; import { createGraphQLClientAbility } from './shared/abilities/graphql-client.ts'; export const CellixApiWorld = registerManagedSerenityWorld({ - infrastructure: { - ensureStarted: infra.ensureApiServers, - getState: infra.getState, - resetScenarioState: infra.resetMongoForScenario, - stopAll: infra.stopAll, - }, + infrastructure, validateState: (state) => { - if (!state.graphqlUrl) { + if (!graphqlUrl(state)) { throw new Error('API acceptance infrastructure did not expose a graphqlUrl'); } }, createCast: (state) => new SerenityCast({ useNotepad: true, - abilities: [() => createGraphQLClientAbility(state.graphqlUrl ?? ''), () => createCommunityAbility()], + abilities: [() => createGraphQLClientAbility(graphqlUrl(state)), () => createCommunityAbility()], }), }); export type CellixApiWorld = InstanceType; registerLifecycleHooks(); + +function graphqlUrl(state: ApiInfrastructureState): string { + // biome-ignore lint:useLiteralKeys - servers is an index-signature record; bracket access required by noPropertyAccessFromIndexSignature + const graphqlServer = state.servers['graphql']; + return graphqlServer?.isRunning() ? graphqlServer.getUrl() : ''; +} diff --git a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts index c5860ec97..d67bc0d9a 100644 --- a/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts +++ b/packages/ocom-verification/e2e-tests/src/contexts/authentication/step-definitions/header-login.steps.ts @@ -1,7 +1,7 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actorCalled, notes } from '@serenity-js/core'; import type { BrowserContext, Page } from 'playwright'; -import * as infra from '../../../infrastructure.ts'; +import { infrastructure } from '../../../infrastructure.ts'; import type { CellixE2EWorld } from '../../../world.ts'; import type { HeaderE2ENotes, HeaderE2ESite } from '../notes/header-notes.ts'; import { ClickHeaderSignIn } from '../tasks/click-header-sign-in.ts'; @@ -65,7 +65,7 @@ When('{word} chooses to sign in', async function (this: HeaderE2EWorld, actorNam // Fresh unauthenticated context for the portal under test — its baseURL is the // portal's own URL. Isolated from the pre-auth context used by other suites; // cleaned up in the Then step after verification. - const context = await infra.newPortalContext(s.site); + const context = await infrastructure.newPortalContext(s.site); s.context = context; if (s.identityProviderUnreachable) { diff --git a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts index 511fc56e0..3b5b23a65 100644 --- a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts @@ -4,7 +4,7 @@ import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber import { registerScreenshotOnFailureHook } from '@cellix/serenity-framework/cucumber/screenshot'; import { getTimeout } from '@cellix/serenity-framework/settings'; import type { IWorld } from '@cucumber/cucumber'; -import * as infra from './infrastructure.ts'; +import { infrastructure } from './infrastructure.ts'; import type { CellixE2EWorld } from './world.ts'; const currentDir = fileURLToPath(new URL('.', import.meta.url)); @@ -19,7 +19,7 @@ export function registerLifecycleHooks(): void { after: async (world) => { await world.cleanup(); }, - afterAll: () => infra.stopAll(), + afterAll: () => infrastructure.stopAll(), }); registerScreenshotOnFailureHook({ diff --git a/packages/ocom-verification/e2e-tests/src/infrastructure.ts b/packages/ocom-verification/e2e-tests/src/infrastructure.ts index 89d401faa..cbd4c797d 100644 --- a/packages/ocom-verification/e2e-tests/src/infrastructure.ts +++ b/packages/ocom-verification/e2e-tests/src/infrastructure.ts @@ -1,44 +1,17 @@ -import { E2EInfrastructure, type E2EInfrastructureState } from '@cellix/serenity-framework/infrastructure/e2e'; -import { MongoMemoryTestServer } from '@cellix/serenity-framework/servers'; -import { getMongoPort } from '@ocom-verification/verification-shared/environment'; -import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; -import type { BrowserContext } from 'playwright'; -import { cleanupTestEnvironment, createCommunityUiPortalServer, createStaffUiPortalServer, createTestApiServer, createTestAzuriteServer, createTestOAuth2Server, initTestEnvironment } from './test-server-factories.ts'; +import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; +import { communityUiPortalServer, staffUiPortalServer, testApiServer, testAzuriteServer, testMongoServer, testOAuth2Server } from './servers/index.ts'; +import { cleanupTestEnvironment, initTestEnvironment } from './shared/environment/test-environment.ts'; -const apiDbName = 'owner-community'; - -const infrastructure = E2EInfrastructure.create({ +export const infrastructure = E2EInfrastructure.create({ // baseURL is supplied per portal by the framework; only shared options here. browserContextOptions: { ignoreHTTPSErrors: true }, cleanupEnvironment: cleanupTestEnvironment, setupEnvironment: initTestEnvironment, }) - .addServer('mongo', () => new MongoMemoryTestServer({ dbName: apiDbName, port: getMongoPort(), replSetName: 'globaldb', seedData: seedDatabase }), { - resetForScenario: (server) => (server as MongoMemoryTestServer).resetForScenario(), - }) - .addServer('azurite', () => createTestAzuriteServer()) - .addServer('auth', () => createTestOAuth2Server()) - .addServer('api', (ctx) => createTestApiServer(() => ctx.server('mongo').getConnectionString()), { dependsOn: ['mongo'] }) - .addUiPortal('community', () => createCommunityUiPortalServer()) - .addUiPortal('staff', () => createStaffUiPortalServer()); - -export function getState(): E2EInfrastructureState { - return infrastructure.getState(); -} - -export function newPortalContext(site: 'community' | 'staff'): Promise { - return infrastructure.newPortalContext(site); -} - -export async function resetScenarioState(): Promise { - await infrastructure.resetScenarioState(); -} - -export async function stopAll(): Promise { - await infrastructure.stopAll(); -} - -export async function ensureE2EServers(): Promise { - infrastructure.registerProcessShutdownHandlers(); - await infrastructure.ensureStarted(); -} + .addServer('mongo', testMongoServer) + .addServer('azurite', testAzuriteServer) + .addServer('auth', testOAuth2Server) + .addServer('api', testApiServer, { dependsOn: ['mongo', 'azurite', 'auth'] }) + .addUiPortal('community', communityUiPortalServer, { dependsOn: ['api', 'auth'] }) + .addUiPortal('staff', staffUiPortalServer, { dependsOn: ['api', 'auth'] }) + .finalize(); diff --git a/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts b/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts new file mode 100644 index 000000000..88042f08d --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts @@ -0,0 +1,23 @@ +import { ProcessUiTestServer } from '@cellix/serenity-framework/servers'; +import { appPaths } from '../shared/environment/app-paths.ts'; +import { getPortlessDevScript } from '../shared/environment/dev-script.ts'; +import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; + +const hostnames = getHostnames(); + +export const communityUiPortalServer = new ProcessUiTestServer({ + cwd: appPaths.uiCommunityDir, + executable: 'pnpm', + extraEnv: () => ({ + BROWSER: 'none', + NODE_ENV: 'development', + }), + getUrl: () => buildUrl(hostnames.uiCommunity), + isAlreadyRunning: () => Promise.resolve(false), + probe: { + url: () => buildUrl(hostnames.uiCommunity), + }, + readyMarker: 'ready in', + serverName: 'TestCommunityViteServer', + spawnArgs: () => ['run', getPortlessDevScript()], +}); diff --git a/packages/ocom-verification/e2e-tests/src/servers/index.ts b/packages/ocom-verification/e2e-tests/src/servers/index.ts new file mode 100644 index 000000000..11587eb5d --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/index.ts @@ -0,0 +1,6 @@ +export { communityUiPortalServer } from './community-ui-portal-server.ts'; +export { staffUiPortalServer } from './staff-ui-portal-server.ts'; +export { testApiServer } from './test-api-server.ts'; +export { testAzuriteServer } from './test-azurite-server.ts'; +export { testMongoServer } from './test-mongo-server.ts'; +export { testOAuth2Server } from './test-oauth2-server.ts'; diff --git a/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts b/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts new file mode 100644 index 000000000..1490a75ea --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts @@ -0,0 +1,23 @@ +import { ProcessUiTestServer } from '@cellix/serenity-framework/servers'; +import { appPaths } from '../shared/environment/app-paths.ts'; +import { getPortlessDevScript } from '../shared/environment/dev-script.ts'; +import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; + +const hostnames = getHostnames(); + +export const staffUiPortalServer = new ProcessUiTestServer({ + cwd: appPaths.uiStaffDir, + executable: 'pnpm', + extraEnv: () => ({ + BROWSER: 'none', + NODE_ENV: 'development', + }), + getUrl: () => buildUrl(hostnames.uiStaff), + isAlreadyRunning: () => Promise.resolve(false), + probe: { + url: () => buildUrl(hostnames.uiStaff), + }, + readyMarker: 'ready in', + serverName: 'TestStaffViteServer', + spawnArgs: () => ['run', getPortlessDevScript()], +}); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts new file mode 100644 index 000000000..3eef6f06a --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts @@ -0,0 +1,29 @@ +import { ProcessTestServer } from '@cellix/serenity-framework/servers'; +import { appPaths } from '../shared/environment/app-paths.ts'; +import { e2eEnv, getPortlessDevScript } from '../shared/environment/dev-script.ts'; +import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; +import { testMongoServer } from './test-mongo-server.ts'; + +const hostnames = getHostnames(); + +export const testApiServer = new ProcessTestServer({ + cwd: appPaths.apiDir, + executable: 'pnpm', + extraEnv: () => + e2eEnv({ + COSMOSDB_CONNECTION_STRING: testMongoServer.getConnectionString(), + }), + getUrl: () => buildUrl(hostnames.api, '/api/graphql'), + isAlreadyRunning: () => Promise.resolve(false), + probe: { + requestInit: () => ({ + body: JSON.stringify({ query: '{ __typename }' }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }), + url: () => buildUrl(hostnames.api, '/api/graphql'), + }, + readyMarker: 'Functions:', + serverName: 'TestApiServer', + spawnArgs: () => ['run', getPortlessDevScript()], +}); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts new file mode 100644 index 000000000..eed7c4d14 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts @@ -0,0 +1,21 @@ +import { join } from 'node:path'; +import { ProcessTestServer } from '@cellix/serenity-framework/servers'; +import { getAzuritePorts } from '@ocom-verification/verification-shared/environment'; +import { appPaths } from '../shared/environment/app-paths.ts'; + +export const testAzuriteServer = new ProcessTestServer({ + cwd: appPaths.apiDir, + executable: 'node', + extraEnv: () => { + const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); + const { PATH: pathValue = '' } = process.env; + return { PATH: `${binDir}:${pathValue}` }; + }, + getUrl: () => `http://127.0.0.1:${getAzuritePorts().blob}`, + isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), + portsToCloseBeforeStart: () => getAzuritePorts().blob, + probe: false, + readyMarker: '[azurite] started', + serverName: 'TestAzuriteServer', + spawnArgs: ['start-azurite.mjs'], +}); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts new file mode 100644 index 000000000..0b5588685 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts @@ -0,0 +1,25 @@ +import { MongoMemoryProcessTestServer } from '@cellix/serenity-framework/servers'; +import { getMongoPort } from '@ocom-verification/verification-shared/environment'; +import { seedDatabase } from '@ocom-verification/verification-shared/test-data'; +import { appPaths } from '../shared/environment/app-paths.ts'; +import { getPortlessDevScript } from '../shared/environment/dev-script.ts'; + +const mongoDbName = 'owner-community'; +const mongoReplSetName = 'globaldb'; + +export const testMongoServer = new MongoMemoryProcessTestServer({ + connectionString: mongoConnectionString, + cwd: appPaths.mongodbMemoryMockDir, + dbName: mongoDbName, + executable: 'pnpm', + portsToCloseBeforeStart: getMongoPort, + probe: false, + readyMarker: 'MongoDB Memory Replica Set ready at:', + seedData: seedDatabase, + serverName: 'TestMongoMemoryServer', + spawnArgs: () => ['run', getPortlessDevScript()], +}); + +function mongoConnectionString(): string { + return `mongodb://127.0.0.1:${getMongoPort()}/${mongoDbName}?replicaSet=${mongoReplSetName}`; +} diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts new file mode 100644 index 000000000..e64d73724 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts @@ -0,0 +1,17 @@ +import { ProcessTestServer } from '@cellix/serenity-framework/servers'; +import { appPaths } from '../shared/environment/app-paths.ts'; +import { getPortlessDevScript } from '../shared/environment/dev-script.ts'; +import { buildUrl, getHostnames, mockOidcIssuer } from '../shared/environment/test-environment.ts'; + +const hostnames = getHostnames(); + +export const testOAuth2Server = new ProcessTestServer({ + cwd: appPaths.oauth2MockDir, + executable: 'pnpm', + getUrl: () => buildUrl(hostnames.mockAuth, new URL(mockOidcIssuer).pathname), + isAlreadyRunning: () => Promise.resolve(false), + probe: false, + readyMarker: 'Registered OIDC config', + serverName: 'TestOAuth2Server', + spawnArgs: () => ['run', getPortlessDevScript()], +}); diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts index 9a5e789f7..b112e07cb 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/app-paths.ts @@ -6,6 +6,7 @@ const workspaceRoot = resolve(currentDir, '../../../../../..'); export const appPaths = { apiDir: resolve(workspaceRoot, 'apps/api'), + mongodbMemoryMockDir: resolve(workspaceRoot, 'apps/server-mongodb-memory-mock'), oauth2MockDir: resolve(workspaceRoot, 'apps/server-oauth2-mock'), uiCommunityDir: resolve(workspaceRoot, 'apps/ui-community'), uiStaffDir: resolve(workspaceRoot, 'apps/ui-staff'), diff --git a/packages/ocom-verification/e2e-tests/src/test-server-factories.ts b/packages/ocom-verification/e2e-tests/src/test-server-factories.ts deleted file mode 100644 index 83d72cb67..000000000 --- a/packages/ocom-verification/e2e-tests/src/test-server-factories.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { join } from 'node:path'; -import { ProcessTestServer } from '@cellix/serenity-framework/servers'; -import { getAzuritePorts } from '@ocom-verification/verification-shared/environment'; -import { appPaths } from './shared/environment/app-paths.ts'; -import { e2eEnv, getPortlessDevScript } from './shared/environment/dev-script.ts'; -import { buildUrl, getHostnames, mockOidcEndpoint, mockOidcIssuer } from './shared/environment/test-environment.ts'; - -export { cleanupTestEnvironment, initTestEnvironment } from './shared/environment/test-environment.ts'; - -const hostnames = getHostnames(); - -export function createTestApiServer(getMongoConnectionString: () => string): ProcessTestServer { - return new ProcessTestServer({ - cwd: appPaths.apiDir, - executable: 'pnpm', - extraEnv: () => - e2eEnv({ - COSMOSDB_CONNECTION_STRING: getMongoConnectionString(), - }), - getUrl: () => buildUrl(hostnames.api, '/api/graphql'), - probe: { - url: () => buildUrl(hostnames.api, '/api/graphql'), - requestInit: () => ({ - body: JSON.stringify({ query: '{ __typename }' }), - headers: { 'Content-Type': 'application/json' }, - method: 'POST', - }), - isHealthy: async (response) => { - if (!response.ok) { - return false; - } - - const payload = (await response.json().catch(() => null)) as { - data?: { __typename?: string }; - errors?: unknown[]; - } | null; - - return payload?.data?.__typename === 'Query' && !payload.errors?.length; - }, - }, - readyMarker: 'Functions:', - serverName: 'TestApiServer', - spawnArgs: () => ['run', getPortlessDevScript()], - }); -} - -export function createTestAzuriteServer(): ProcessTestServer { - return new ProcessTestServer({ - cwd: appPaths.apiDir, - executable: 'node', - extraEnv: () => { - const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); - const { PATH: pathValue = '' } = process.env; - return { PATH: `${binDir}:${pathValue}` }; - }, - getUrl: () => `http://127.0.0.1:${getAzuritePorts().blob}`, - isAlreadyRunning: async () => false, - isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), - probe: false, - readyMarker: '[azurite] started', - serverName: 'TestAzuriteServer', - spawnArgs: ['start-azurite.mjs'], - }); -} - -export function createTestOAuth2Server(): ProcessTestServer { - return new ProcessTestServer({ - cwd: appPaths.oauth2MockDir, - executable: 'pnpm', - getUrl: () => mockOidcIssuer, - probe: { - url: mockOidcEndpoint, - }, - readyMarker: 'Registered OIDC config', - serverName: 'TestOAuth2Server', - spawnArgs: () => ['run', getPortlessDevScript()], - }); -} - -export function createCommunityUiPortalServer(): ProcessTestServer { - return new ProcessTestServer({ - cwd: appPaths.uiCommunityDir, - executable: 'pnpm', - extraEnv: () => ({ - BROWSER: 'none', - NODE_ENV: 'development', - }), - getUrl: () => buildUrl(hostnames.uiCommunity), - readyMarker: 'ready in', - serverName: 'TestCommunityViteServer', - spawnArgs: () => ['run', getPortlessDevScript()], - }); -} - -export function createStaffUiPortalServer(): ProcessTestServer { - return new ProcessTestServer({ - cwd: appPaths.uiStaffDir, - executable: 'pnpm', - extraEnv: () => ({ - BROWSER: 'none', - NODE_ENV: 'development', - }), - getUrl: () => buildUrl(hostnames.uiStaff), - readyMarker: 'ready in', - serverName: 'TestStaffViteServer', - spawnArgs: () => ['run', getPortlessDevScript()], - }); -} diff --git a/packages/ocom-verification/e2e-tests/src/world.ts b/packages/ocom-verification/e2e-tests/src/world.ts index 2e5d6a44d..62d891c0e 100644 --- a/packages/ocom-verification/e2e-tests/src/world.ts +++ b/packages/ocom-verification/e2e-tests/src/world.ts @@ -1,16 +1,11 @@ import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; import { SerenityCast } from '@cellix/serenity-framework/serenity'; import { registerLifecycleHooks } from './cucumber-lifecycle-hooks.ts'; -import * as infra from './infrastructure.ts'; +import { infrastructure } from './infrastructure.ts'; import { OAuth2Login } from './shared/abilities/oauth2-login.ts'; export const CellixE2EWorld = registerManagedSerenityWorld({ - infrastructure: { - ensureStarted: infra.ensureE2EServers, - getState: infra.getState, - resetScenarioState: infra.resetScenarioState, - stopAll: infra.stopAll, - }, + infrastructure, validateState: (state) => { if (!state.browseTheWeb) { throw new Error('BrowseTheWeb ability not initialized'); diff --git a/packages/ocom-verification/verification-shared/package.json b/packages/ocom-verification/verification-shared/package.json index d2873cc32..a53110a4e 100644 --- a/packages/ocom-verification/verification-shared/package.json +++ b/packages/ocom-verification/verification-shared/package.json @@ -5,7 +5,6 @@ "private": true, "type": "module", "exports": { - "./abilities": "./src/abilities/index.ts", "./environment": "./src/environment/index.ts", "./pages": "./src/pages/index.ts", "./test-data": "./src/test-data/index.ts" diff --git a/packages/ocom-verification/verification-shared/src/abilities/create-community.ts b/packages/ocom-verification/verification-shared/src/abilities/create-community.ts deleted file mode 100644 index a53c51cb7..000000000 --- a/packages/ocom-verification/verification-shared/src/abilities/create-community.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Ability, type Actor } from '@serenity-js/core'; - -/** Community details accepted by OCOM verification flows. */ -export interface CreateCommunityDetails { - /** Community display name. */ - name: string; -} - -/** Result returned by a concrete community creation flow. */ -export interface CreateCommunityResult { - /** Created community id, when the flow exposes one. */ - id?: string; - - /** Created community name. */ - name: string; -} - -/** Handler that performs community creation for an actor. */ -export type CreateCommunityHandler = (actor: Actor, details: CreateCommunityDetails) => Promise; - -/** - * Serenity ability that lets an actor create OCOM communities. - * - * The ability centralizes the domain capability while allowing each verification - * package to provide the environment-specific implementation. - */ -export class CreateCommunity extends Ability { - /** - * @param handler Function that performs community creation. - */ - constructor(private readonly handler: CreateCommunityHandler) { - super(); - } - - /** - * Create the ability from an environment-specific community creation handler. - * - * @param handler Function that performs community creation. - */ - static using(handler: CreateCommunityHandler): CreateCommunity { - return new CreateCommunity(handler); - } - - /** - * Create a community through the configured verification environment. - * - * @param actor Actor creating the community. - * @param details Community details. - */ - async performAs(actor: Actor, details: CreateCommunityDetails): Promise { - return await this.handler(actor, details); - } -} diff --git a/packages/ocom-verification/verification-shared/src/abilities/index.ts b/packages/ocom-verification/verification-shared/src/abilities/index.ts deleted file mode 100644 index 65388a4ab..000000000 --- a/packages/ocom-verification/verification-shared/src/abilities/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { CreateCommunityDetails, CreateCommunityHandler, CreateCommunityResult } from './create-community.ts'; -export { CreateCommunity } from './create-community.ts'; From b3d9969b3f979cb17627d8cdb311dc348aa33c97 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 9 Jun 2026 09:58:31 -0400 Subject: [PATCH 23/40] some adjustments to package pattern --- .gitignore | 1 + packages/cellix/local-dev/README.md | 2 + packages/cellix/local-dev/manifest.md | 3 +- packages/cellix/local-dev/src/dotenv.ts | 14 ++- packages/cellix/local-dev/src/index.test.ts | 10 +- packages/cellix/local-dev/src/index.ts | 1 + packages/cellix/local-dev/src/json-files.ts | 12 ++ packages/cellix/local-dev/src/runners.ts | 33 ++++++ packages/cellix/local-dev/src/urls.ts | 72 +++++++++++- .../cellix/local-dev/src/worktree-ports.ts | 23 ++++ packages/ocom/local-dev-config/README.md | 49 ++++++++ packages/ocom/local-dev-config/manifest.md | 59 ++++++++++ packages/ocom/local-dev-config/package.json | 10 +- .../ocom/local-dev-config/src/hostnames.ts | 21 ++++ .../ocom/local-dev-config/src/index.test.ts | 111 ++++++++++++++++++ packages/ocom/local-dev-config/src/types.ts | 26 ++++ packages/ocom/local-dev-config/src/urls.ts | 14 +++ .../ocom/local-dev-config/src/workspace.ts | 7 ++ .../local-dev-config/tsconfig.vitest.json | 10 ++ .../ocom/local-dev-config/vitest.config.ts | 19 +++ pnpm-lock.yaml | 9 ++ 21 files changed, 497 insertions(+), 9 deletions(-) create mode 100644 packages/ocom/local-dev-config/README.md create mode 100644 packages/ocom/local-dev-config/manifest.md create mode 100644 packages/ocom/local-dev-config/src/index.test.ts create mode 100644 packages/ocom/local-dev-config/tsconfig.vitest.json create mode 100644 packages/ocom/local-dev-config/vitest.config.ts diff --git a/.gitignore b/.gitignore index 33786a672..eb6cc48e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +.pnpm-store .DS_Store **/coverage # .gitignore template - https://github.com/github/gitignore/blob/main/Node.gitignore diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 3fe57fdab..83cb2ee8e 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -72,6 +72,7 @@ syncJsonFile({ - `writeJsonFile` - `syncJsonFile` - `hostnameFromUrl` +- `sanitizeWorktreeHostnameLabel` - `applyWorktreeSuffix` - `buildPortlessUrl` - `replaceUrlPort` @@ -93,4 +94,5 @@ syncJsonFile({ ## Notes - The package derives workspace roots from the caller's current working directory, but it does not infer app layouts or env-variable names. +- Worktree names are sanitized before they are inserted into `.localhost` hostnames so branch-style names such as `jason/my-feature` become DNS-safe labels such as `jason-my-feature`. Suffixing is idempotent for hostnames that already contain the sanitized worktree label. - If a helper only exists to support one app's local policy, it should usually live with that app instead of being exported here. diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index d577a0634..95afe2332 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -20,7 +20,7 @@ This package owns generic worktree-aware port math, URL helpers, dotenv and JSON - `resolveWorkspaceRoot(options?)` - `readDotEnv(filePath)` - `readJsonFile(filePath)`, `writeJsonFile(filePath, data)`, `syncJsonFile(options)` -- `hostnameFromUrl(url)`, `applyWorktreeSuffix(hostname, worktreeName)`, `buildPortlessUrl(hostname, path?)`, `replaceUrlPort(url, port)`, `PORTLESS_PORT` +- `hostnameFromUrl(url)`, `sanitizeWorktreeHostnameLabel(worktreeName)`, `applyWorktreeSuffix(hostname, worktreeName)`, `buildPortlessUrl(hostname, path?)`, `replaceUrlPort(url, port)`, `PORTLESS_PORT` - `isE2E(env?)`, `buildViteArgs(options?)` - `isGracefulInterruptExit(signal, code)`, `forwardChildExit(child)` - `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)`, `buildAzuriteConnectionString(options)` @@ -31,6 +31,7 @@ This package owns generic worktree-aware port math, URL helpers, dotenv and JSON - App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and settings-file transforms. - This package should expose only reusable mechanics that make those wrappers smaller and more consistent. - Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, and hostname suffixing aligned across all participating apps. +- Hostname suffixing must sanitize raw worktree names before inserting them into `.localhost` domains, and repeated suffixing with the same worktree label must leave hostnames unchanged. ## Package boundaries diff --git a/packages/cellix/local-dev/src/dotenv.ts b/packages/cellix/local-dev/src/dotenv.ts index 5dca2928d..5f8e322ef 100644 --- a/packages/cellix/local-dev/src/dotenv.ts +++ b/packages/cellix/local-dev/src/dotenv.ts @@ -5,7 +5,13 @@ export type DotEnvValues = Record; /** * Reads a dotenv-style file into a plain key/value object. * - * Lines without `=` and comment lines are ignored. + * This helper intentionally supports only the portable subset needed by local + * wrapper scripts: `KEY=value` lines, optional `export` prefixes, comments, + * empty lines, and matching single or double quotes around values. It does not + * expand variables or execute shell syntax. + * + * @param filePath - Dotenv file to read. + * @returns Parsed values, or an empty object when the file does not exist. */ export function readDotEnv(filePath: string): DotEnvValues { if (!existsSync(filePath)) { @@ -24,7 +30,11 @@ export function readDotEnv(filePath: string): DotEnvValues { continue; } - values[trimmed.slice(0, separatorIndex)] = trimmed.slice(separatorIndex + 1); + const key = trimmed.slice(0, separatorIndex).replace(/^export\s+/, ''); + const rawValue = trimmed.slice(separatorIndex + 1); + const quote = rawValue.at(0); + const value = (quote === '"' || quote === "'") && rawValue.endsWith(quote) ? rawValue.slice(1, -1) : rawValue; + values[key] = value; } return values; diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 4f79f00f9..923db1322 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -14,6 +14,7 @@ import { readDotEnv, replaceUrlPort, resolveWorkspaceRoot, + sanitizeWorktreeHostnameLabel, syncJsonFile, } from '@cellix/local-dev'; import { describe, expect, it } from 'vitest'; @@ -42,7 +43,7 @@ describe('@cellix/local-dev', () => { it('parses dotenv values and applies worktree-aware URL helpers generically', () => { const workspaceRoot = createWorkspaceFixture(); const envPath = path.join(workspaceRoot, 'fixtures', '.env'); - writeFileSync(envPath, ['BASE_URL=https://ownercommunity.localhost:1355', 'API_URL=https://data-access.ownercommunity.localhost:1355/api/graphql'].join('\n')); + writeFileSync(envPath, ['export BASE_URL="https://ownercommunity.localhost:1355"', "API_URL='https://data-access.ownercommunity.localhost:1355/api/graphql'", 'IGNORED_LINE_WITHOUT_SEPARATOR'].join('\n')); const envValues = readDotEnv(envPath) as DotEnvFixtureValues; const baseUrl = envValues.BASE_URL; @@ -58,7 +59,12 @@ describe('@cellix/local-dev', () => { throw new Error('Expected dotenv fixture values to be defined'); } expect(hostnameFromUrl(baseUrl)).toBe('ownercommunity.localhost'); - expect(applyWorktreeSuffix('ownercommunity.localhost', 'feature-123')).toBe('ownercommunity.feature-123.localhost'); + expect(sanitizeWorktreeHostnameLabel('Jason/Feature 123')).toBe('jason-feature-123'); + expect(sanitizeWorktreeHostnameLabel('---')).toBeUndefined(); + expect(applyWorktreeSuffix('ownercommunity.localhost', 'Jason/Feature 123')).toBe('ownercommunity.jason-feature-123.localhost'); + expect(applyWorktreeSuffix('ownercommunity.jason-feature-123.localhost', 'Jason/Feature 123')).toBe('ownercommunity.jason-feature-123.localhost'); + expect(applyWorktreeSuffix('localhost', 'Jason/Feature 123')).toBe('jason-feature-123.localhost'); + expect(applyWorktreeSuffix('example.com', 'feature-123')).toBe('example.com'); expect(buildPortlessUrl('ownercommunity.localhost')).toBe(`https://ownercommunity.localhost:${PORTLESS_PORT}`); expect(replaceUrlPort(apiUrl, 50900)).toBe('https://data-access.ownercommunity.localhost:50900/api/graphql'); }); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index 53edbb87e..c30c35820 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -26,6 +26,7 @@ export { hostnameFromUrl, PORTLESS_PORT, replaceUrlPort, + sanitizeWorktreeHostnameLabel, } from './urls.ts'; export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite.ts'; export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; diff --git a/packages/cellix/local-dev/src/json-files.ts b/packages/cellix/local-dev/src/json-files.ts index 4c9c5296c..12f7a3156 100644 --- a/packages/cellix/local-dev/src/json-files.ts +++ b/packages/cellix/local-dev/src/json-files.ts @@ -2,13 +2,19 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from import path from 'node:path'; export interface SyncJsonFileOptions { + /** Source JSON document to copy or transform. */ sourcePath: string; + /** Destination path. Parent directories are created automatically. */ targetPath: string; + /** Optional consumer-owned transform applied before writing the target. */ transform?: (data: TData) => TData; } /** * Reads a JSON file and returns its parsed contents. + * + * @param filePath - JSON file to parse. + * @returns Parsed JSON typed by the caller. */ export function readJsonFile(filePath: string): TData { return JSON.parse(readFileSync(filePath, 'utf8')) as TData; @@ -16,6 +22,10 @@ export function readJsonFile(filePath: string): TData { /** * Writes a JSON document with stable indentation and a trailing newline. + * + * @param filePath - Destination path. Parent directories are created + * automatically. + * @param data - JSON-serializable document to write. */ export function writeJsonFile(filePath: string, data: unknown): void { mkdirSync(path.dirname(filePath), { recursive: true }); @@ -24,6 +34,8 @@ export function writeJsonFile(filePath: string, data: unknown): void { /** * Copies a JSON file to a target location and optionally applies a transform. + * + * @param options - Source, target, and optional transform owned by the caller. */ export function syncJsonFile(options: SyncJsonFileOptions): void { mkdirSync(path.dirname(options.targetPath), { recursive: true }); diff --git a/packages/cellix/local-dev/src/runners.ts b/packages/cellix/local-dev/src/runners.ts index 1afa324f1..777b6e4eb 100644 --- a/packages/cellix/local-dev/src/runners.ts +++ b/packages/cellix/local-dev/src/runners.ts @@ -12,27 +12,40 @@ type RunnerEnv = NodeJS.ProcessEnv & { }; export interface RunnerOptions { + /** Environment to pass to the spawned process. Defaults to `process.env`. */ env?: NodeJS.ProcessEnv; } export interface TsxRunnerOptions extends RunnerOptions { + /** TSX entrypoint to execute. Defaults to `src/index.ts`. */ entry?: string; } export interface AzureFunctionsDevOptions extends RunnerOptions { + /** Functions host port. Defaults to `env.PORT`; required after resolution. */ port?: string; + /** Azure Functions script root. Defaults to `deploy/`. */ scriptRoot?: string; + /** CORS value passed to the Functions host. Defaults to `*`. */ cors?: string; + /** Whether to pass `--typescript`. Defaults to `true`. */ typescript?: boolean; } export interface AzuriteDevOptions extends RunnerOptions { + /** Blob service port. */ blobPort: number; + /** Queue service port. */ queuePort: number; + /** Table service port. */ tablePort: number; + /** Blob storage working directory. */ blobLocation: string; + /** Queue storage working directory. */ queueLocation: string; + /** Table storage working directory. */ tableLocation: string; + /** Whether Azurite should run quietly. Defaults to `true`. */ silent?: boolean; } @@ -45,6 +58,10 @@ function spawnInherited(command: string, args: string[], options: { env?: NodeJS /** * Starts a Vite dev process using the caller-provided environment. + * + * @param options - Optional environment overrides. `HOST`, `PORT`, `E2E`, and + * `E2E_VITE_MODE` are interpreted by `buildViteArgs`. + * @returns The spawned Vite child process. */ export function runViteDev(options: RunnerOptions = {}): ChildProcess { const env = (options.env ?? process.env) as RunnerEnv; @@ -63,6 +80,10 @@ export function runViteDev(options: RunnerOptions = {}): ChildProcess { /** * Starts the Docusaurus dev server with the shared local-dev defaults. + * + * @param options - Optional environment overrides. `PORT` controls the internal + * Docusaurus listener and defaults to `3001`. + * @returns The spawned Docusaurus child process. */ export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { const env = (options.env ?? process.env) as RunnerEnv; @@ -74,6 +95,11 @@ export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { /** * Starts an Azure Functions dev process using caller-supplied runtime * configuration and environment variables. + * + * @param options - Functions host options plus optional environment overrides. + * `PORT` must be provided directly or through the environment. + * @returns The spawned Functions host child process. + * @throws When no port can be resolved. */ export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): ChildProcess { const env = (options.env ?? process.env) as RunnerEnv; @@ -104,6 +130,9 @@ export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): Ch /** * Starts a TSX-backed dev process using the caller-provided entrypoint and * environment. + * + * @param options - Optional entrypoint and environment overrides. + * @returns The spawned TSX child process. */ export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { const env = options.env ?? process.env; @@ -115,6 +144,10 @@ export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { /** * Starts the three Azurite worker processes using caller-supplied ports and * storage paths. + * + * @param options - Explicit Azurite ports, storage locations, and optional + * environment. + * @returns The spawned blob, queue, and table child processes. */ export function runAzuriteDev(options: AzuriteDevOptions): ChildProcess[] { const procSpecs: Array<[string, string[]]> = [ diff --git a/packages/cellix/local-dev/src/urls.ts b/packages/cellix/local-dev/src/urls.ts index 8b4b71b73..78a2ec514 100644 --- a/packages/cellix/local-dev/src/urls.ts +++ b/packages/cellix/local-dev/src/urls.ts @@ -1,8 +1,42 @@ export const PORTLESS_PORT = 1355; +const LOCALHOST_SUFFIX_PATTERN = /(^|\.)localhost$/; + +/** + * Converts a worktree name into a DNS-label-safe hostname segment. + * + * The returned value is lower-case, replaces non-alphanumeric separators with + * `-`, trims leading/trailing dashes, and is capped to one DNS label. When the + * input cannot produce a valid label, `undefined` is returned so callers can + * leave the base hostname unchanged. + * + * @param worktreeName - Raw worktree identifier, often copied from a branch or + * directory name. + * @returns A hostname-safe label, or `undefined` when no usable label exists. + * + * @example + * ```ts + * sanitizeWorktreeHostnameLabel('jason/feature one'); + * // 'jason-feature-one' + * ``` + */ +export function sanitizeWorktreeHostnameLabel(worktreeName: string | undefined): string | undefined { + const label = worktreeName + ?.toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 63) + .replace(/-+$/g, ''); + + return label || undefined; +} + /** * Returns the hostname portion of a URL string, or `null` when the value is not * a valid URL. + * + * @param url - Absolute URL string to inspect. + * @returns The parsed hostname, or `null` for invalid URL strings. */ export function hostnameFromUrl(url: string): string | null { try { @@ -14,17 +48,47 @@ export function hostnameFromUrl(url: string): string | null { /** * Applies a worktree suffix to a `.localhost` hostname. + * + * The worktree segment is sanitized before use so raw branch-style names such + * as `jason/my-feature` do not create invalid local hostnames. Hostnames that + * do not end in `.localhost` are returned unchanged. + * + * @param hostname - Base local hostname, for example `ownercommunity.localhost`. + * @param worktreeName - Optional raw worktree identifier. + * @returns The hostname with a worktree label inserted before `.localhost`. + * + * @example + * ```ts + * applyWorktreeSuffix('ownercommunity.localhost', 'jason/my-feature'); + * // 'ownercommunity.jason-my-feature.localhost' + * ``` */ export function applyWorktreeSuffix(hostname: string, worktreeName: string | undefined): string { - if (!worktreeName) { + const label = sanitizeWorktreeHostnameLabel(worktreeName); + if (!label || !LOCALHOST_SUFFIX_PATTERN.test(hostname)) { + return hostname; + } + + if (hostname === 'localhost') { + return `${label}.localhost`; + } + + const localhostIndex = hostname.lastIndexOf('.localhost'); + const labelsBeforeLocalhost = hostname.slice(0, localhostIndex).split('.'); + if (labelsBeforeLocalhost.at(-1) === label) { return hostname; } - return hostname.replace('.localhost', `.${worktreeName}.localhost`); + return hostname.replace(LOCALHOST_SUFFIX_PATTERN, `.${label}.localhost`); } /** * Builds a portless-proxied HTTPS URL for a hostname and optional path. + * + * @param hostname - Public local hostname managed by portless. + * @param relativePath - Optional path to append. Include the leading slash when + * a path is needed. + * @returns An HTTPS URL using Cellix's local portless port. */ export function buildPortlessUrl(hostname: string, relativePath = ''): string { return `https://${hostname}:${PORTLESS_PORT}${relativePath}`; @@ -32,6 +96,10 @@ export function buildPortlessUrl(hostname: string, relativePath = ''): string { /** * Replaces the port component of a URL and returns the updated string. + * + * @param url - Absolute URL string to update. + * @param port - Replacement port number or numeric string. + * @returns The updated URL string. */ export function replaceUrlPort(url: string, port: number | string): string { const parsedUrl = new URL(url); diff --git a/packages/cellix/local-dev/src/worktree-ports.ts b/packages/cellix/local-dev/src/worktree-ports.ts index 493c20fb3..7d17a6824 100644 --- a/packages/cellix/local-dev/src/worktree-ports.ts +++ b/packages/cellix/local-dev/src/worktree-ports.ts @@ -1,6 +1,9 @@ export interface AzuritePorts { + /** Blob service port. */ blob: number; + /** Queue service port. */ queue: number; + /** Table service port. */ table: number; } @@ -14,6 +17,14 @@ function getDefaultWorktreeName(): string | undefined { /** * Returns a deterministic worktree port offset in increments of 100. + * + * The default worktree uses offset `0`. Named worktrees use one of 49 buckets + * from `100` through `4900`, which keeps them away from the default service + * ports while remaining deterministic for repeat local runs. + * + * @param worktreeName - Optional worktree identifier. Defaults to + * `process.env.WORKTREE_NAME`. + * @returns Port offset to add to service base ports. */ export function getWorktreePortOffset(worktreeName = getDefaultWorktreeName()): number { if (!worktreeName) return 0; @@ -28,6 +39,10 @@ export function getWorktreePortOffset(worktreeName = getDefaultWorktreeName()): /** * Returns the MongoDB port for the current worktree. + * + * @param worktreeName - Optional worktree identifier. Defaults to + * `process.env.WORKTREE_NAME`. + * @returns MongoDB port derived from base `50000` plus the worktree offset. */ export function getMongoPort(worktreeName = getDefaultWorktreeName()): number { return 50000 + getWorktreePortOffset(worktreeName); @@ -35,6 +50,11 @@ export function getMongoPort(worktreeName = getDefaultWorktreeName()): number { /** * Returns the Azurite ports for the current worktree. + * + * @param worktreeName - Optional worktree identifier. Defaults to + * `process.env.WORKTREE_NAME`. + * @returns Blob, queue, and table ports derived from Azurite's default ports + * plus the worktree offset. */ export function getAzuritePorts(worktreeName = getDefaultWorktreeName()): AzuritePorts { const offset = getWorktreePortOffset(worktreeName); @@ -49,6 +69,9 @@ export function getAzuritePorts(worktreeName = getDefaultWorktreeName()): Azurit /** * Builds an Azurite connection string from explicit account credentials and * ports supplied by the consumer. + * + * @param options - Account credentials and explicit Azurite ports to encode. + * @returns A connection string for the selected worktree-scoped Azurite ports. */ export function buildAzuriteConnectionString(options: { accountName: string; accountKey: string; ports: AzuritePorts; host?: string }): string { const host = options.host ?? '127.0.0.1'; diff --git a/packages/ocom/local-dev-config/README.md b/packages/ocom/local-dev-config/README.md new file mode 100644 index 000000000..9e22cc015 --- /dev/null +++ b/packages/ocom/local-dev-config/README.md @@ -0,0 +1,49 @@ +# @ocom/local-dev-config + +OCOM-specific local-development URL and hostname helpers built on `@cellix/local-dev`. + +This package owns Owner Community application policy: which app `.env` files define local hostnames, which auth issuer paths are used, and which URLs app wrapper scripts should inject. Generic mechanics such as dotenv parsing, portless URL construction, and worktree-safe hostname suffixing stay in `@cellix/local-dev`. + +## Install + +```json +{ + "devDependencies": { + "@ocom/local-dev-config": "workspace:*" + } +} +``` + +## Usage + +Use this package from app-owned `start-dev.ts` or settings-sync wrappers: + +```ts +import { runViteDev } from '@cellix/local-dev'; +import { buildOcomUrls } from '@ocom/local-dev-config'; + +const urls = buildOcomUrls(); + +runViteDev({ + env: { + ...process.env, + VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, + VITE_APP_UI_COMMUNITY_BASE_URL: urls.uiCommunityBaseUrl, + }, +}); +``` + +## Public API + +- `getOcomHostnames(options?)` +- `buildOcomUrls(options?)` +- `getWorkspaceRoot(startDir?)` +- `OcomLocalDevOptions` +- `OcomHostnames` +- `OcomUrls` + +## Boundaries + +- Keep reusable process runners, dotenv parsing, JSON syncing, and port math in `@cellix/local-dev`. +- Keep OCOM-specific hostname derivation, auth paths, redirect paths, and app `.env` lookup policy here. +- Keep one-off runtime behavior in the consuming app wrapper script instead of widening this package. diff --git a/packages/ocom/local-dev-config/manifest.md b/packages/ocom/local-dev-config/manifest.md new file mode 100644 index 000000000..533fcd55b --- /dev/null +++ b/packages/ocom/local-dev-config/manifest.md @@ -0,0 +1,59 @@ +# manifest.md - @ocom/local-dev-config + +## Purpose + +Provide Owner Community local-development URL and hostname policy as a reusable package, while delegating generic local-development mechanics to `@cellix/local-dev`. + +## Scope + +This package resolves OCOM hostnames from app `.env` files and environment overrides, applies worktree-safe host suffixing through `@cellix/local-dev`, and builds the complete local URL set needed by UI, API, mock-auth, and docs wrapper scripts. + +## Non-goals + +- Generic process runners, port math, JSON syncing, or dotenv parsing +- Production runtime configuration +- Non-OCOM app defaults +- Starting local dev processes directly + +## Public API shape + +- `getOcomHostnames(options?)` +- `buildOcomUrls(options?)` +- `getWorkspaceRoot(startDir?)` +- `OcomLocalDevOptions` +- `OcomHostnames` +- `OcomUrls` + +## Core concepts + +- OCOM app wrappers should compose this package with `@cellix/local-dev`. +- Environment values override app `.env` file values so task runners can inject per-process configuration. +- Worktree suffixing is delegated to `@cellix/local-dev` so all participating apps share one hostname-safety and idempotency rule. + +## Package boundaries + +- App-specific auth paths and redirect paths are allowed here because they are OCOM local-dev policy. +- Generic local-development primitives must not be reimplemented here. +- One-off behavior for a single app should stay in that app's wrapper script unless at least two OCOM consumers need the same policy. + +## Dependencies / relationships + +- Depends on `@cellix/local-dev` +- Downstream consumers in this branch: `@apps/api`, `@apps/ui-community`, `@apps/ui-staff`, and `@apps/server-oauth2-mock` + +## Testing strategy + +- Test through the package entrypoint. +- Prove hostnames are derived from fixture app `.env` files, environment overrides take precedence, worktree suffixes are applied safely without duplication, and the full URL contract remains stable. + +## Documentation obligations + +- Keep `README.md` consumer-facing and focused on app-wrapper usage. +- Keep this manifest aligned with the exported package contract and OCOM-specific boundaries. +- Maintain TSDoc on exported helpers and public option/result types. + +## Release-readiness standards + +- Package build and tests must pass. +- The public surface should remain small and policy-focused. +- Any expansion of exported URL fields should be justified by a real app-wrapper consumer. diff --git a/packages/ocom/local-dev-config/package.json b/packages/ocom/local-dev-config/package.json index 0f60c1930..33937e308 100644 --- a/packages/ocom/local-dev-config/package.json +++ b/packages/ocom/local-dev-config/package.json @@ -33,15 +33,21 @@ "clean": "rimraf dist", "lint": "biome lint", "format": "biome format --write", - "format:check": "biome format ." + "format:check": "biome format .", + "test": "vitest run --silent --reporter=dot", + "test:coverage": "vitest run --coverage --silent --reporter=dot", + "test:watch": "vitest" }, "dependencies": { "@cellix/local-dev": "workspace:*" }, "devDependencies": { "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@types/node": "catalog:", + "@vitest/coverage-istanbul": "catalog:", "rimraf": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom/local-dev-config/src/hostnames.ts b/packages/ocom/local-dev-config/src/hostnames.ts index 191cb173e..c39aa56ef 100644 --- a/packages/ocom/local-dev-config/src/hostnames.ts +++ b/packages/ocom/local-dev-config/src/hostnames.ts @@ -25,6 +25,27 @@ function readAppEnv(workspaceRoot: string, appName: string): DotEnvValues & Ocom return readDotEnv(path.join(workspaceRoot, 'apps', appName, '.env')) as DotEnvValues & OcomEnvValues; } +/** + * Resolves the OCOM local-development hostnames from app `.env` files and + * optional process environment overrides. + * + * The package keeps OCOM-specific policy here: which app env files to read, + * which env keys define the public local hostnames, and how the documentation + * hostname is derived. Generic URL parsing and worktree suffixing remain in + * `@cellix/local-dev`. + * + * @param options - Optional environment and workspace-root overrides. + * @returns Worktree-aware OCOM hostnames. + * @throws When a required URL cannot be found or parsed. + * + * @example + * ```ts + * const hostnames = getOcomHostnames({ + * env: { WORKTREE_NAME: 'jason/feature' }, + * workspaceRoot, + * }); + * ``` + */ export function getOcomHostnames(options: OcomLocalDevOptions = {}): OcomHostnames { const env = (options.env ?? process.env) as NodeJS.ProcessEnv & OcomEnvValues; const workspaceRoot = options.workspaceRoot ?? getWorkspaceRoot(); diff --git a/packages/ocom/local-dev-config/src/index.test.ts b/packages/ocom/local-dev-config/src/index.test.ts new file mode 100644 index 000000000..090fdd712 --- /dev/null +++ b/packages/ocom/local-dev-config/src/index.test.ts @@ -0,0 +1,111 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { buildOcomUrls, getOcomHostnames, getWorkspaceRoot } from '@ocom/local-dev-config'; +import { describe, expect, it } from 'vitest'; + +function createWorkspaceFixture(): string { + const workspaceRoot = mkdtempSync(path.join(tmpdir(), 'ocom-local-dev-config-')); + writeFileSync(path.join(workspaceRoot, 'pnpm-workspace.yaml'), 'packages:\n - "apps/*"\n'); + mkdirSync(path.join(workspaceRoot, 'apps', 'ui-community'), { recursive: true }); + mkdirSync(path.join(workspaceRoot, 'apps', 'ui-staff'), { recursive: true }); + writeFileSync( + path.join(workspaceRoot, 'apps', 'ui-community', '.env'), + [ + 'VITE_APP_UI_COMMUNITY_BASE_URL=https://ownercommunity.localhost:1355', + 'VITE_COMMON_API_ENDPOINT=https://data-access.ownercommunity.localhost:1355/api/graphql', + 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY=https://mock-auth.ownercommunity.localhost:1355/community', + ].join('\n'), + ); + writeFileSync(path.join(workspaceRoot, 'apps', 'ui-staff', '.env'), 'VITE_APP_UI_STAFF_BASE_URL=https://staff.ownercommunity.localhost:1355\n'); + + return workspaceRoot; +} + +describe('@ocom/local-dev-config', () => { + it('resolves OCOM hostnames from app env files and applies a safe worktree suffix', () => { + const workspaceRoot = createWorkspaceFixture(); + + expect( + getOcomHostnames({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + workspaceRoot, + }), + ).toEqual({ + uiCommunity: 'ownercommunity.jason-feature-123.localhost', + uiStaff: 'staff.ownercommunity.jason-feature-123.localhost', + api: 'data-access.ownercommunity.jason-feature-123.localhost', + mockAuth: 'mock-auth.ownercommunity.jason-feature-123.localhost', + docs: 'docs.ownercommunity.jason-feature-123.localhost', + }); + }); + + it('builds the complete OCOM local URL set for app wrapper scripts', () => { + const workspaceRoot = createWorkspaceFixture(); + + expect(buildOcomUrls({ env: {}, workspaceRoot })).toEqual({ + uiCommunityBaseUrl: 'https://ownercommunity.localhost:1355', + uiCommunityRedirectUrl: 'https://ownercommunity.localhost:1355/auth-redirect', + uiStaffBaseUrl: 'https://staff.ownercommunity.localhost:1355', + uiStaffRedirectUrl: 'https://staff.ownercommunity.localhost:1355/auth-redirect', + apiGraphqlUrl: 'https://data-access.ownercommunity.localhost:1355/api/graphql', + mockCommunityAuthorityUrl: 'https://mock-auth.ownercommunity.localhost:1355/community', + mockCommunityJwksUrl: 'https://mock-auth.ownercommunity.localhost:1355/community/.well-known/jwks.json', + mockStaffAuthorityUrl: 'https://mock-auth.ownercommunity.localhost:1355/staff', + mockStaffJwksUrl: 'https://mock-auth.ownercommunity.localhost:1355/staff/.well-known/jwks.json', + docsBaseUrl: 'https://docs.ownercommunity.localhost:1355', + }); + }); + + it('resolves the workspace root from a nested OCOM package directory', () => { + const workspaceRoot = createWorkspaceFixture(); + const nestedDir = path.join(workspaceRoot, 'apps', 'ui-community'); + + expect(getWorkspaceRoot(nestedDir)).toBe(workspaceRoot); + }); + + it('lets environment values override app env files', () => { + const workspaceRoot = createWorkspaceFixture(); + + expect( + getOcomHostnames({ + env: { + VITE_APP_UI_COMMUNITY_BASE_URL: 'https://community.override.localhost:1355', + VITE_COMMON_API_ENDPOINT: 'https://api.override.localhost:1355/api/graphql', + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: 'https://auth.override.localhost:1355/community', + VITE_APP_UI_STAFF_BASE_URL: 'https://staff.override.localhost:1355', + }, + workspaceRoot, + }), + ).toEqual({ + uiCommunity: 'community.override.localhost', + uiStaff: 'staff.override.localhost', + api: 'api.override.localhost', + mockAuth: 'auth.override.localhost', + docs: 'docs.community.override.localhost', + }); + }); + + it('does not duplicate a worktree suffix when hostnames are already scoped', () => { + const workspaceRoot = createWorkspaceFixture(); + + expect( + getOcomHostnames({ + env: { + WORKTREE_NAME: 'Jason/Feature 123', + VITE_APP_UI_COMMUNITY_BASE_URL: 'https://ownercommunity.jason-feature-123.localhost:1355', + VITE_COMMON_API_ENDPOINT: 'https://data-access.ownercommunity.jason-feature-123.localhost:1355/api/graphql', + VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', + VITE_APP_UI_STAFF_BASE_URL: 'https://staff.ownercommunity.jason-feature-123.localhost:1355', + }, + workspaceRoot, + }), + ).toEqual({ + uiCommunity: 'ownercommunity.jason-feature-123.localhost', + uiStaff: 'staff.ownercommunity.jason-feature-123.localhost', + api: 'data-access.ownercommunity.jason-feature-123.localhost', + mockAuth: 'mock-auth.ownercommunity.jason-feature-123.localhost', + docs: 'docs.ownercommunity.jason-feature-123.localhost', + }); + }); +}); diff --git a/packages/ocom/local-dev-config/src/types.ts b/packages/ocom/local-dev-config/src/types.ts index b862ea512..a09750de2 100644 --- a/packages/ocom/local-dev-config/src/types.ts +++ b/packages/ocom/local-dev-config/src/types.ts @@ -1,25 +1,51 @@ +/** + * Options shared by OCOM local-development config helpers. + */ export interface OcomLocalDevOptions { + /** Environment override source. Defaults to `process.env`. */ env?: NodeJS.ProcessEnv; + /** Workspace root to read app `.env` files from. Defaults to auto-discovery. */ workspaceRoot?: string; } +/** + * Portless hostnames used by OCOM browser-facing apps and local HTTP mocks. + */ export interface OcomHostnames { + /** Community portal hostname. */ uiCommunity: string; + /** Staff portal hostname. */ uiStaff: string; + /** API hostname. */ api: string; + /** Mock authentication server hostname. */ mockAuth: string; + /** Documentation site hostname. */ docs: string; } +/** + * Fully qualified local URLs consumed by OCOM app wrappers. + */ export interface OcomUrls { + /** Community portal base URL. */ uiCommunityBaseUrl: string; + /** Community portal OIDC redirect URL. */ uiCommunityRedirectUrl: string; + /** Staff portal base URL. */ uiStaffBaseUrl: string; + /** Staff portal OIDC redirect URL. */ uiStaffRedirectUrl: string; + /** API GraphQL endpoint URL. */ apiGraphqlUrl: string; + /** Community OIDC issuer URL on the mock auth server. */ mockCommunityAuthorityUrl: string; + /** Community OIDC JWKS URL on the mock auth server. */ mockCommunityJwksUrl: string; + /** Staff OIDC issuer URL on the mock auth server. */ mockStaffAuthorityUrl: string; + /** Staff OIDC JWKS URL on the mock auth server. */ mockStaffJwksUrl: string; + /** Documentation site base URL. */ docsBaseUrl: string; } diff --git a/packages/ocom/local-dev-config/src/urls.ts b/packages/ocom/local-dev-config/src/urls.ts index 2302d5d32..88231ae2d 100644 --- a/packages/ocom/local-dev-config/src/urls.ts +++ b/packages/ocom/local-dev-config/src/urls.ts @@ -2,6 +2,20 @@ import { buildPortlessUrl } from '@cellix/local-dev'; import { getOcomHostnames } from './hostnames.ts'; import type { OcomLocalDevOptions, OcomUrls } from './types.ts'; +/** + * Builds the full set of local URLs required by OCOM app wrapper scripts. + * + * @param options - Optional environment and workspace-root overrides forwarded + * to `getOcomHostnames`. + * @returns Portless HTTPS URLs for UI apps, API GraphQL, mock OIDC issuers, + * JWKS endpoints, and docs. + * + * @example + * ```ts + * const urls = buildOcomUrls(); + * process.env.VITE_COMMON_API_ENDPOINT = urls.apiGraphqlUrl; + * ``` + */ export function buildOcomUrls(options: OcomLocalDevOptions = {}): OcomUrls { const hostnames = getOcomHostnames(options); diff --git a/packages/ocom/local-dev-config/src/workspace.ts b/packages/ocom/local-dev-config/src/workspace.ts index 2c66b424e..a8db0065a 100644 --- a/packages/ocom/local-dev-config/src/workspace.ts +++ b/packages/ocom/local-dev-config/src/workspace.ts @@ -1,5 +1,12 @@ import { resolveWorkspaceRoot } from '@cellix/local-dev'; +/** + * Resolves the OCOM workspace root using the shared Cellix workspace lookup. + * + * @param startDir - Directory to start searching from. Defaults to the current + * working directory. + * @returns Absolute workspace root path. + */ export function getWorkspaceRoot(startDir: string = process.cwd()): string { return resolveWorkspaceRoot({ startDir }); } diff --git a/packages/ocom/local-dev-config/tsconfig.vitest.json b/packages/ocom/local-dev-config/tsconfig.vitest.json new file mode 100644 index 000000000..fc31246f1 --- /dev/null +++ b/packages/ocom/local-dev-config/tsconfig.vitest.json @@ -0,0 +1,10 @@ +{ + "extends": ["./tsconfig.json", "@cellix/config-typescript/vitest"], + "compilerOptions": { + "paths": { + "@ocom/local-dev-config": ["./src/index.ts"], + "@cellix/local-dev": ["../../cellix/local-dev/src/index.ts"] + } + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/ocom/local-dev-config/vitest.config.ts b/packages/ocom/local-dev-config/vitest.config.ts new file mode 100644 index 000000000..422905798 --- /dev/null +++ b/packages/ocom/local-dev-config/vitest.config.ts @@ -0,0 +1,19 @@ +import { join } from 'node:path'; +import { getDirnameFromImportMetaUrl, nodeConfig } from '@cellix/config-vitest'; +import { mergeConfig } from 'vitest/config'; + +const dirname = getDirnameFromImportMetaUrl(import.meta.url); + +export default mergeConfig(nodeConfig, { + test: { + typecheck: { + tsconfig: './tsconfig.vitest.json', + }, + }, + resolve: { + alias: { + '@ocom/local-dev-config': join(dirname, 'src/index.ts'), + '@cellix/local-dev': join(dirname, '../../cellix/local-dev/src/index.ts'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a68aba219..a0b6fd4b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1661,15 +1661,24 @@ importers: '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@types/node': specifier: 'catalog:' version: 22.19.15 + '@vitest/coverage-istanbul': + specifier: 'catalog:' + version: 4.1.6(vitest@4.1.6) rimraf: specifier: 'catalog:' version: 6.0.1 typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.7.1)(@emnapi/runtime@1.7.1)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/persistence: dependencies: From b28f193ac56053ad2e1cc638303c7cf552eaa9f1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 9 Jun 2026 14:41:11 -0400 Subject: [PATCH 24/40] restart build - snyk error, unrelated --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 886223cc6..8da72eac1 100644 --- a/readme.md +++ b/readme.md @@ -351,6 +351,7 @@ npm init -w ./packages/event-bus-seedwork-node npm install --save-dev @tsconfig/node20 @tsconfig/node-ts vitest @vitest/coverage-istanbul ``` + ## Your feedback matters! Do you find CellixJS useful? [Give us a ⭐ star on GitHub](https://github.com/cellixjs/cellixjs)! From 8aa14d0c85cf66bc1c595cd245843b5a72d691e1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 11 Jun 2026 13:37:07 -0400 Subject: [PATCH 25/40] adjusted local dev patterns to follow object pattern and extract majority of actual logic to cellix package, while centralizing possible urls, etc for portless in ocom package. --- .snyk | 10 + apps/api/package.json | 8 +- apps/api/rolldown.config.ts | 2 +- apps/api/src/index.ts | 20 +- .../src/service-config/apollo-server/index.ts | 2 +- apps/api/start-azurite.ts | 17 +- apps/api/start-dev.ts | 7 +- apps/api/sync-local-settings.ts | 66 +---- apps/server-mongodb-memory-mock/package.json | 2 +- apps/server-mongodb-memory-mock/start-dev.ts | 11 +- apps/server-oauth2-mock/package.json | 3 +- apps/server-oauth2-mock/start-dev.ts | 9 +- apps/ui-community/package.json | 2 +- apps/ui-community/start-dev.ts | 9 +- apps/ui-staff/package.json | 2 +- apps/ui-staff/start-dev.ts | 9 +- knip.json | 13 +- packages/cellix/local-dev/README.md | 79 ++++-- packages/cellix/local-dev/manifest.md | 30 ++- packages/cellix/local-dev/package.json | 28 ++ .../local-dev/src/{ => files}/dotenv.ts | 0 packages/cellix/local-dev/src/files/index.ts | 2 + .../src/{json-files.ts => files/json.ts} | 0 packages/cellix/local-dev/src/index.test.ts | 247 ++++++++++++++++++ packages/cellix/local-dev/src/index.ts | 36 ++- .../src/{dev-process.ts => process/index.ts} | 0 packages/cellix/local-dev/src/runners.ts | 203 -------------- .../runners/azure-functions-local-settings.ts | 78 ++++++ .../local-dev/src/runners/azure-functions.ts | 123 +++++++++ .../cellix/local-dev/src/runners/azurite.ts | 153 +++++++++++ .../local-dev/src/runners/docusaurus.ts | 47 ++++ .../cellix/local-dev/src/runners/index.ts | 7 + packages/cellix/local-dev/src/runners/node.ts | 71 +++++ .../cellix/local-dev/src/runners/spawn.ts | 17 ++ .../cellix/local-dev/src/runners/types.ts | 34 +++ packages/cellix/local-dev/src/runners/vite.ts | 58 ++++ .../local-dev/src/{urls.ts => urls/index.ts} | 0 .../local-dev/src/{vite.ts => vite/index.ts} | 9 + .../src/{workspace.ts => workspace/index.ts} | 0 .../cellix/local-dev/src/worktree/index.ts | 10 + .../local-dev/src/worktree/json-file-sync.ts | 89 +++++++ .../{worktree-ports.ts => worktree/ports.ts} | 0 .../cellix/local-dev/src/worktree/settings.ts | 117 +++++++++ .../cellix/local-dev/src/worktree/types.ts | 57 ++++ .../local-dev/src/worktree/worktree-name.ts | 31 +++ packages/ocom/local-dev-config/README.md | 21 +- packages/ocom/local-dev-config/manifest.md | 17 +- packages/ocom/local-dev-config/package.json | 12 +- .../src/api-settings/index.ts | 31 +++ .../src/{hostnames.ts => hostnames/index.ts} | 11 +- .../ocom/local-dev-config/src/index.test.ts | 9 +- packages/ocom/local-dev-config/src/index.ts | 6 +- .../src/{urls.ts => urls/index.ts} | 8 +- .../ocom/local-dev-config/src/workspace.ts | 12 - .../ocom/local-dev-config/vitest.config.ts | 17 +- pnpm-lock.yaml | 21 +- pnpm-workspace.yaml | 1 + 57 files changed, 1447 insertions(+), 437 deletions(-) rename packages/cellix/local-dev/src/{ => files}/dotenv.ts (100%) create mode 100644 packages/cellix/local-dev/src/files/index.ts rename packages/cellix/local-dev/src/{json-files.ts => files/json.ts} (100%) rename packages/cellix/local-dev/src/{dev-process.ts => process/index.ts} (100%) delete mode 100644 packages/cellix/local-dev/src/runners.ts create mode 100644 packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts create mode 100644 packages/cellix/local-dev/src/runners/azure-functions.ts create mode 100644 packages/cellix/local-dev/src/runners/azurite.ts create mode 100644 packages/cellix/local-dev/src/runners/docusaurus.ts create mode 100644 packages/cellix/local-dev/src/runners/index.ts create mode 100644 packages/cellix/local-dev/src/runners/node.ts create mode 100644 packages/cellix/local-dev/src/runners/spawn.ts create mode 100644 packages/cellix/local-dev/src/runners/types.ts create mode 100644 packages/cellix/local-dev/src/runners/vite.ts rename packages/cellix/local-dev/src/{urls.ts => urls/index.ts} (100%) rename packages/cellix/local-dev/src/{vite.ts => vite/index.ts} (69%) rename packages/cellix/local-dev/src/{workspace.ts => workspace/index.ts} (100%) create mode 100644 packages/cellix/local-dev/src/worktree/index.ts create mode 100644 packages/cellix/local-dev/src/worktree/json-file-sync.ts rename packages/cellix/local-dev/src/{worktree-ports.ts => worktree/ports.ts} (100%) create mode 100644 packages/cellix/local-dev/src/worktree/settings.ts create mode 100644 packages/cellix/local-dev/src/worktree/types.ts create mode 100644 packages/cellix/local-dev/src/worktree/worktree-name.ts create mode 100644 packages/ocom/local-dev-config/src/api-settings/index.ts rename packages/ocom/local-dev-config/src/{hostnames.ts => hostnames/index.ts} (86%) rename packages/ocom/local-dev-config/src/{urls.ts => urls/index.ts} (83%) delete mode 100644 packages/ocom/local-dev-config/src/workspace.ts diff --git a/.snyk b/.snyk index f9cb17e99..922028793 100644 --- a/.snyk +++ b/.snyk @@ -101,3 +101,13 @@ ignore: reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. Created task to upgrade OTEL service to 2.x and resolve vulnerability that way.' expires: '2026-07-28T00:00:00.000Z' created: '2026-06-01T10:00:00.000Z' + 'SNYK-JS-IMAGESIZE-17295814': + - '* > image-size': + reason: 'Transitive dependency in @docusaurus/mdx-loader and dev build tooling; no upgrade or patch available from upstream. Not exploitable in current usage (build-time image dimension parsing only).' + expires: '2026-09-11T00:00:00.000Z' + created: '2026-06-11T00:00:00.000Z' + 'SNYK-JS-IMAGESIZE-17295816': + - '* > image-size': + reason: 'Transitive dependency in @docusaurus/mdx-loader and dev build tooling; no upgrade or patch available from upstream. Not exploitable in current usage (build-time image dimension parsing only).' + expires: '2026-09-11T00:00:00.000Z' + created: '2026-06-11T00:00:00.000Z' diff --git a/apps/api/package.json b/apps/api/package.json index ec7246dd2..bf35766ab 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -8,10 +8,10 @@ "scripts": { "prebuild": "pnpm run lint", "build": "tsgo --build && rolldown -c rolldown.config.ts", - "predev": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.ts", - "predev:worktree": "pnpm run prepare:deploy && pnpm run sync-local-settings", - "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", + "predev": "pnpm run prepare:deploy", + "dev": "CELLIX_WORKTREE=0 pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.ts", + "predev:worktree": "pnpm run prepare:deploy", + "dev:worktree": "CELLIX_WORKTREE=1 pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", diff --git a/apps/api/rolldown.config.ts b/apps/api/rolldown.config.ts index 3d69fd482..e4a5be380 100644 --- a/apps/api/rolldown.config.ts +++ b/apps/api/rolldown.config.ts @@ -10,10 +10,10 @@ * packages when workspace packages are in the module graph. */ -import { defineConfig } from 'rolldown'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createCellixAzureFunctionsRolldownConfig } from '@cellix/config-rolldown'; +import { defineConfig } from 'rolldown'; const apiDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(apiDir, '../..'); diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 3bdc43e68..b3a9a8329 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,23 +1,19 @@ import './service-config/otel-starter.ts'; -import { Cellix } from './cellix.ts'; -import type { ApiContextSpec } from '@ocom/context-spec'; import { type ApplicationServices, buildApplicationServicesFactory } from '@ocom/application-services'; +import type { ApiContextSpec } from '@ocom/context-spec'; import { RegisterEventHandlers } from '@ocom/event-handler'; - -import { ServiceMongoose } from '@ocom/service-mongoose'; -import * as MongooseConfig from './service-config/mongoose/index.ts'; +import { type GraphContext, graphHandlerCreator } from '@ocom/graphql-handler'; +import { restHandlerCreator } from '@ocom/rest'; +import { ServiceApolloServer } from '@ocom/service-apollo-server'; import { ServiceBlobStorage } from '@ocom/service-blob-storage'; - +import { ServiceMongoose } from '@ocom/service-mongoose'; import { ServiceTokenValidation } from '@ocom/service-token-validation'; -import * as TokenValidationConfig from './service-config/token-validation/index.ts'; - -import { ServiceApolloServer } from '@ocom/service-apollo-server'; +import { Cellix } from './cellix.ts'; import * as ApolloServerConfig from './service-config/apollo-server/index.ts'; - -import { graphHandlerCreator, type GraphContext } from '@ocom/graphql-handler'; -import { restHandlerCreator } from '@ocom/rest'; +import * as MongooseConfig from './service-config/mongoose/index.ts'; +import * as TokenValidationConfig from './service-config/token-validation/index.ts'; Cellix.initializeInfrastructureServices((serviceRegistry) => { serviceRegistry diff --git a/apps/api/src/service-config/apollo-server/index.ts b/apps/api/src/service-config/apollo-server/index.ts index fa09139af..ea3a0366e 100644 --- a/apps/api/src/service-config/apollo-server/index.ts +++ b/apps/api/src/service-config/apollo-server/index.ts @@ -1,5 +1,5 @@ -import type { ServiceApolloServerOptions } from '@ocom/service-apollo-server'; import { combinedSchema, permissions } from '@ocom/graphql'; +import type { ServiceApolloServerOptions } from '@ocom/service-apollo-server'; const { NODE_ENV } = process.env; const isDev = NODE_ENV !== 'production'; diff --git a/apps/api/start-azurite.ts b/apps/api/start-azurite.ts index a95930199..b45ce9a2c 100644 --- a/apps/api/start-azurite.ts +++ b/apps/api/start-azurite.ts @@ -1,16 +1,3 @@ -import path from 'node:path'; -import { getAzuritePorts, resolveWorkspaceRoot, runAzuriteDev } from '@cellix/local-dev'; +import { AzuriteDevRunner } from '@cellix/local-dev'; -const workspaceRoot = resolveWorkspaceRoot(); -const worktreeName = process.env['WORKTREE_NAME']; -const ports = getAzuritePorts(worktreeName); -const storageSuffix = worktreeName ? `-${worktreeName}` : ''; - -runAzuriteDev({ - blobPort: ports.blob, - blobLocation: path.join(workspaceRoot, `__blobstorage__${storageSuffix}`), - queuePort: ports.queue, - queueLocation: path.join(workspaceRoot, `__queuestorage__${storageSuffix}`), - tablePort: ports.table, - tableLocation: path.join(workspaceRoot, `__tablestorage__${storageSuffix}`), -}); +new AzuriteDevRunner().start(); diff --git a/apps/api/start-dev.ts b/apps/api/start-dev.ts index f637f6bc0..b58d1a220 100644 --- a/apps/api/start-dev.ts +++ b/apps/api/start-dev.ts @@ -1,3 +1,6 @@ -import { runAzureFunctionsDev } from '@cellix/local-dev'; +import { AzureFunctionsDevRunner } from '@cellix/local-dev'; +import { buildOcomApiLocalSettings } from '@ocom/local-dev-config'; -runAzureFunctionsDev(); +new AzureFunctionsDevRunner({ + localSettings: buildOcomApiLocalSettings(), +}).start(); diff --git a/apps/api/sync-local-settings.ts b/apps/api/sync-local-settings.ts index 6998ba94c..5cdd1b83b 100644 --- a/apps/api/sync-local-settings.ts +++ b/apps/api/sync-local-settings.ts @@ -1,64 +1,4 @@ -import { existsSync } from 'node:fs'; -import path from 'node:path'; -import { buildAzuriteConnectionString, getAzuritePorts, getMongoPort, replaceUrlPort, resolveWorkspaceRoot, syncJsonFile } from '@cellix/local-dev'; -import { buildOcomUrls } from '@ocom/local-dev-config'; +import { AzureFunctionsLocalSettings } from '@cellix/local-dev'; +import { buildOcomApiLocalSettings } from '@ocom/local-dev-config'; -const workspaceRoot = resolveWorkspaceRoot(); -const appDir = process.cwd(); -const env = process.env; -const isE2E = ['1', 'true', 'yes'].includes((env['E2E'] ?? '').toLowerCase()); -const localSettingsPath = path.join(appDir, 'local.settings.json'); -const e2eLocalSettingsPath = path.join(appDir, 'local-settings.e2e.json'); -const targetPath = path.join(appDir, 'deploy', 'local.settings.json'); - -if (!isE2E) { - if (!existsSync(localSettingsPath)) { - process.exit(0); - } - - syncJsonFile({ - sourcePath: localSettingsPath, - targetPath, - }); - process.exit(0); -} - -const urls = buildOcomUrls({ env, workspaceRoot }); -const worktreeName = env['WORKTREE_NAME']; -const ports = getAzuritePorts(worktreeName); - -syncJsonFile({ - sourcePath: e2eLocalSettingsPath, - targetPath, - transform: (document) => { - const settings = document; - const values = { ...(settings.Values ?? {}) }; - const accountName = String(values['STORAGE_ACCOUNT_NAME'] ?? ''); - const accountKey = String(values['STORAGE_ACCOUNT_KEY'] ?? ''); - - values['ACCOUNT_PORTAL_OIDC_ISSUER'] = urls.mockCommunityAuthorityUrl; - values['ACCOUNT_PORTAL_OIDC_ENDPOINT'] = urls.mockCommunityJwksUrl; - values['STAFF_PORTAL_OIDC_ISSUER'] = urls.mockStaffAuthorityUrl; - values['STAFF_PORTAL_OIDC_ENDPOINT'] = urls.mockStaffJwksUrl; - - if (accountName && accountKey && worktreeName) { - const azuriteConnectionString = buildAzuriteConnectionString({ - accountKey, - accountName, - ports, - }); - values['AZURE_STORAGE_CONNECTION_STRING'] = azuriteConnectionString; - values['AzureWebJobsStorage'] = azuriteConnectionString; - } - - const cosmosConnectionString = env['COSMOSDB_CONNECTION_STRING'] ?? values['COSMOSDB_CONNECTION_STRING']; - if (typeof cosmosConnectionString === 'string' && cosmosConnectionString) { - values['COSMOSDB_CONNECTION_STRING'] = worktreeName ? replaceUrlPort(cosmosConnectionString, getMongoPort(worktreeName)) : cosmosConnectionString; - } - - return { - ...settings, - Values: values, - }; - }, -}); +new AzureFunctionsLocalSettings(buildOcomApiLocalSettings()).sync(); diff --git a/apps/server-mongodb-memory-mock/package.json b/apps/server-mongodb-memory-mock/package.json index 9cf2e1ba9..1e3502fef 100644 --- a/apps/server-mongodb-memory-mock/package.json +++ b/apps/server-mongodb-memory-mock/package.json @@ -11,7 +11,7 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "node start-dev.ts", + "dev": "node src/index.ts", "dev:worktree": "node start-dev.ts" }, "dependencies": { diff --git a/apps/server-mongodb-memory-mock/start-dev.ts b/apps/server-mongodb-memory-mock/start-dev.ts index a1afd8bd2..a77256137 100644 --- a/apps/server-mongodb-memory-mock/start-dev.ts +++ b/apps/server-mongodb-memory-mock/start-dev.ts @@ -1,8 +1,7 @@ -import { getMongoPort, runTsxDev } from '@cellix/local-dev'; +import { NodeDevRunner } from '@cellix/local-dev'; -runTsxDev({ - env: { - ...process.env, - PORT: String(getMongoPort(process.env['WORKTREE_NAME'])), +new NodeDevRunner({ + settings: { + PORT: '50000', }, -}); +}).start(); diff --git a/apps/server-oauth2-mock/package.json b/apps/server-oauth2-mock/package.json index 28e3fadc3..0d810fa79 100644 --- a/apps/server-oauth2-mock/package.json +++ b/apps/server-oauth2-mock/package.json @@ -11,7 +11,7 @@ "format": "biome format --write", "format:check": "biome format .", "start": "node dist/index.js", - "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force node start-dev.ts", + "dev": "pnpm exec portless mock-auth.ownercommunity.localhost --force node src/index.ts", "dev:worktree": "pnpm exec portless mock-auth.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "test": "vitest run", "test:coverage": "vitest run --coverage", @@ -28,7 +28,6 @@ "@cellix/config-typescript": "workspace:*", "@vitest/coverage-istanbul": "catalog:", "rimraf": "catalog:", - "tsx": "catalog:", "typescript": "catalog:", "vitest": "catalog:" } diff --git a/apps/server-oauth2-mock/start-dev.ts b/apps/server-oauth2-mock/start-dev.ts index d26144b72..37a0a7528 100644 --- a/apps/server-oauth2-mock/start-dev.ts +++ b/apps/server-oauth2-mock/start-dev.ts @@ -1,14 +1,13 @@ /// -import { runTsxDev } from '@cellix/local-dev'; +import { NodeDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; const urls = buildOcomUrls(); -runTsxDev({ - env: { - ...process.env, +new NodeDevRunner({ + settings: { BASE_URL: urls.mockCommunityAuthorityUrl.replace('/community', ''), VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: urls.uiCommunityRedirectUrl, VITE_APP_UI_STAFF_AAD_REDIRECT_URI: urls.uiStaffRedirectUrl, }, -}); +}).start(); diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 0631bfbaf..36043e903 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -9,7 +9,7 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless ownercommunity.localhost --force node start-dev.ts", + "dev": "pnpm exec portless ownercommunity.localhost --force vite", "dev:worktree": "pnpm exec portless ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "start": "vite", "preview": "vite preview", diff --git a/apps/ui-community/start-dev.ts b/apps/ui-community/start-dev.ts index 47cf4d758..3017f8859 100644 --- a/apps/ui-community/start-dev.ts +++ b/apps/ui-community/start-dev.ts @@ -1,15 +1,14 @@ /// -import { runViteDev } from '@cellix/local-dev'; +import { ViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; const urls = buildOcomUrls(); -runViteDev({ - env: { - ...process.env, +new ViteDevRunner({ + settings: { VITE_APP_UI_COMMUNITY_B2C_AUTHORITY: urls.mockCommunityAuthorityUrl, VITE_APP_UI_COMMUNITY_B2C_REDIRECT_URI: urls.uiCommunityRedirectUrl, VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, VITE_APP_UI_COMMUNITY_BASE_URL: urls.uiCommunityBaseUrl, }, -}); +}).start(); diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index e270f814a..df6341849 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -9,7 +9,7 @@ "format:check": "biome format .", "prebuild": "pnpm run lint", "build": "tsgo --build && vite build", - "dev": "pnpm exec portless staff.ownercommunity.localhost --force node start-dev.ts", + "dev": "pnpm exec portless staff.ownercommunity.localhost --force vite", "dev:worktree": "pnpm exec portless staff.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "start": "vite", "preview": "vite preview", diff --git a/apps/ui-staff/start-dev.ts b/apps/ui-staff/start-dev.ts index f77a3550b..0257cd6b0 100644 --- a/apps/ui-staff/start-dev.ts +++ b/apps/ui-staff/start-dev.ts @@ -1,15 +1,14 @@ /// -import { runViteDev } from '@cellix/local-dev'; +import { ViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; const urls = buildOcomUrls(); -runViteDev({ - env: { - ...process.env, +new ViteDevRunner({ + settings: { VITE_APP_UI_STAFF_AAD_AUTHORITY: urls.mockStaffAuthorityUrl, VITE_APP_UI_STAFF_AAD_REDIRECT_URI: urls.uiStaffRedirectUrl, VITE_APP_UI_STAFF_BASE_URL: urls.uiStaffBaseUrl, VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, }, -}); +}).start(); diff --git a/knip.json b/knip.json index 9192ef18c..7039a2739 100644 --- a/knip.json +++ b/knip.json @@ -8,15 +8,13 @@ }, "apps/ui-community": { "entry": ["src/main.tsx", "start-dev.ts"], - "project": ["src/**/*.{ts,tsx}", "start-dev.ts"], - "ignore": ["**/apollo-client-links.tsx"], - "ignoreDependencies": ["@cellix/local-dev"] + "project": ["src/**/*.{ts,tsx}"], + "ignore": ["**/apollo-client-links.tsx"] }, "apps/ui-staff": { "entry": ["src/main.tsx", "start-dev.ts"], - "project": ["src/**/*.{ts,tsx}", "start-dev.ts"], - "ignore": [], - "ignoreDependencies": ["@cellix/local-dev"] + "project": ["src/**/*.{ts,tsx}"], + "ignore": [] }, "apps/docs": { "entry": ["src/**/*.{ts,tsx,js,jsx}"], @@ -92,8 +90,7 @@ }, "apps/server-oauth2-mock": { "entry": ["src/index.ts", "start-dev.ts"], - "project": ["src/**/*.ts", "start-dev.ts"], - "ignoreDependencies": ["@cellix/local-dev", "tsx"] + "project": ["src/**/*.ts"] }, "apps/server-mongodb-memory-mock": { "entry": ["src/index.ts"], diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 83cb2ee8e..c6ed0e1fa 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -23,11 +23,12 @@ In this monorepo, app packages consume the workspace package directly: - Worktree-aware hostname and URL utilities - Worktree-aware MongoDB and Azurite port derivation - Azurite connection-string construction from explicit credentials +- Worktree-aware settings transforms for arbitrary env and JSON values - Generic dev runners for: - Vite - Docusaurus - Azure Functions - - TSX-backed processes + - Node-backed processes - Azurite ## Recommended consumption pattern @@ -35,37 +36,65 @@ In this monorepo, app packages consume the workspace package directly: Keep the app policy in a wrapper script and compose this package's generic helpers inside it: ```js -import { buildPortlessUrl, getMongoPort, runTsxDev } from '@cellix/local-dev'; +import { buildPortlessUrl, WorktreeNodeDevRunner } from '@cellix/local-dev'; -runTsxDev({ - env: { - ...process.env, +new WorktreeNodeDevRunner({ + settings: { BASE_URL: buildPortlessUrl('mock-auth.example.localhost'), - PORT: String(getMongoPort(process.env.WORKTREE_NAME)), + PORT: '50000', }, -}); +}).start(); ``` -For settings files, let the app decide what to change: +For settings files, let the app decide the keys and values. The shared syncer applies worktree URL suffixes, Mongo port offsets, and requested Azurite connection strings: ```js -import { syncJsonFile } from '@cellix/local-dev'; +import { WorktreeJsonFileSync } from '@cellix/local-dev'; -syncJsonFile({ +new WorktreeJsonFileSync({ sourcePath: 'local-settings.e2e.json', targetPath: 'deploy/local.settings.json', - transform: (document) => ({ - ...document, - Values: { - ...(document.Values ?? {}), - MODE: 'e2e', + values: { + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.example.localhost:1355/community', + }, + azuriteConnectionStringKeys: ['AzureWebJobsStorage'], +}).sync(); +``` + +Azure Functions can do this directly in the runner so `func start` sees the prepared `local.settings.json` in its script root: + +```js +import { WorktreeAzureFunctionsDevRunner } from '@cellix/local-dev'; + +new WorktreeAzureFunctionsDevRunner({ + localSettings: { + e2eValues: { + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.example.localhost:1355/community', }, - }), -}); + azuriteConnectionStringKeys: ['AzureWebJobsStorage'], + }, +}).start(); ``` +Worktree transforms are enabled when a worktree name is available, either through +`worktreeName` or `WORKTREE_NAME`. Set `worktree: false` or +`CELLIX_WORKTREE=0` for regular dev scripts that should ignore an ambient +`WORKTREE_NAME`; set `worktree: true` or `CELLIX_WORKTREE=1` for explicit +worktree scripts. + ## Public API +All exports are available from `@cellix/local-dev`. Folder-level subpaths are +also published for consumers that want narrower imports: + +- `@cellix/local-dev/files` +- `@cellix/local-dev/process` +- `@cellix/local-dev/runners` +- `@cellix/local-dev/urls` +- `@cellix/local-dev/vite` +- `@cellix/local-dev/workspace` +- `@cellix/local-dev/worktree` + - `resolveWorkspaceRoot` - `readDotEnv` - `readJsonFile` @@ -81,6 +110,19 @@ syncJsonFile({ - `isE2E` - `forwardChildExit` - `isGracefulInterruptExit` +- `ViteDevRunner` +- `DocusaurusDevRunner` +- `AzureFunctionsDevRunner` +- `NodeDevRunner` +- `AzuriteDevRunner` +- `WorktreeSettings` +- `WorktreeViteDevRunner` +- `WorktreeAzureFunctionsDevRunner` +- `WorktreeAzureFunctionsLocalSettings` +- `WorktreeMode` +- `WorktreeNodeDevRunner` +- `WorktreeAzuriteDevRunner` +- `WorktreeJsonFileSync` - `getWorktreePortOffset` - `getMongoPort` - `getAzuritePorts` @@ -88,8 +130,9 @@ syncJsonFile({ - `runViteDev` - `runDocusaurusDev` - `runAzureFunctionsDev` -- `runTsxDev` +- `runNodeDev` - `runAzuriteDev` +- `runTsxDev` deprecated compatibility alias ## Notes diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index 95afe2332..567f064b5 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -6,7 +6,7 @@ Provide generic local-development primitives for Cellix app packages so app-owne ## Scope -This package owns generic worktree-aware port math, URL helpers, dotenv and JSON file utilities, child-process lifecycle handling, and generic dev-runner orchestration for Vite, Docusaurus, Azure Functions, TSX-based processes, and Azurite. +This package owns generic worktree-aware port math, URL helpers, dotenv and JSON file utilities, child-process lifecycle handling, worktree settings transforms, and generic dev-runner orchestration for Vite, Docusaurus, Azure Functions, Node-backed processes, and Azurite. ## Non-goals @@ -17,6 +17,19 @@ This package owns generic worktree-aware port math, URL helpers, dotenv and JSON ## Public API shape +Published entrypoints: + +- `@cellix/local-dev` +- `@cellix/local-dev/files` +- `@cellix/local-dev/process` +- `@cellix/local-dev/runners` +- `@cellix/local-dev/urls` +- `@cellix/local-dev/vite` +- `@cellix/local-dev/workspace` +- `@cellix/local-dev/worktree` + +Root entrypoint exports: + - `resolveWorkspaceRoot(options?)` - `readDotEnv(filePath)` - `readJsonFile(filePath)`, `writeJsonFile(filePath, data)`, `syncJsonFile(options)` @@ -24,18 +37,23 @@ This package owns generic worktree-aware port math, URL helpers, dotenv and JSON - `isE2E(env?)`, `buildViteArgs(options?)` - `isGracefulInterruptExit(signal, code)`, `forwardChildExit(child)` - `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)`, `buildAzuriteConnectionString(options)` -- `runViteDev(options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runTsxDev(options?)`, `runAzuriteDev(options)` +- `ViteDevRunner`, `DocusaurusDevRunner`, `AzureFunctionsDevRunner`, `NodeDevRunner`, `AzuriteDevRunner` +- `WorktreeSettings`, `WorktreeViteDevRunner`, `WorktreeAzureFunctionsDevRunner`, `WorktreeAzureFunctionsLocalSettings`, `WorktreeNodeDevRunner`, `WorktreeAzuriteDevRunner`, `WorktreeJsonFileSync`, `WorktreeMode` +- `runViteDev(options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runNodeDev(options?)`, `runAzuriteDev(options)` +- `runTsxDev(options?)` remains as a deprecated compatibility alias for `runNodeDev(options?)` ## Core concepts -- App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and settings-file transforms. +- App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and which settings values are passed into the worktree transformer. - This package should expose only reusable mechanics that make those wrappers smaller and more consistent. -- Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, and hostname suffixing aligned across all participating apps. +- Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, hostname suffixing, URL-like env values, and JSON settings aligned across all participating apps. +- Worktree transforms are explicit when `worktree` or `CELLIX_WORKTREE` is set; otherwise helpers fall back to applying transforms only when a worktree name is available. +- Azure Functions dev runners may prepare `local.settings.json` before startup because the Functions host reads settings from its script root rather than the process environment alone. - Hostname suffixing must sanitize raw worktree names before inserting them into `.localhost` domains, and repeated suffixing with the same worktree label must leave hostnames unchanged. ## Package boundaries -- Do not encode OCOM app names, env-variable names, or `local.settings.json` schemas in this package. +- Do not encode OCOM app names, auth paths, env-variable names, or `local.settings.json` schemas in this package. - Keep repo-specific hostname and env mapping logic in app-owned scripts or internal repo helpers outside this package. - Avoid widening the public surface with one-off helpers that only exist to support a single app branch. @@ -46,7 +64,7 @@ This package owns generic worktree-aware port math, URL helpers, dotenv and JSON ## Testing strategy -- Prefer public-entrypoint tests for dotenv parsing, URL helpers, worktree port derivation, Vite arg building, Azurite connection-string building, and generic JSON syncing. +- Prefer public-entrypoint tests for dotenv parsing, URL helpers, worktree port derivation, runner object spawning, Vite arg building, Azurite connection-string building, worktree settings transforms, and generic JSON syncing. - Avoid tests that prove repo-specific wrapper policy through this package's public contract. ## Documentation obligations diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json index a1ea2b63e..68ebde17a 100644 --- a/packages/cellix/local-dev/package.json +++ b/packages/cellix/local-dev/package.json @@ -13,6 +13,34 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" + }, + "./files": { + "types": "./dist/files/index.d.ts", + "default": "./dist/files/index.js" + }, + "./process": { + "types": "./dist/process/index.d.ts", + "default": "./dist/process/index.js" + }, + "./runners": { + "types": "./dist/runners/index.d.ts", + "default": "./dist/runners/index.js" + }, + "./urls": { + "types": "./dist/urls/index.d.ts", + "default": "./dist/urls/index.js" + }, + "./vite": { + "types": "./dist/vite/index.d.ts", + "default": "./dist/vite/index.js" + }, + "./workspace": { + "types": "./dist/workspace/index.d.ts", + "default": "./dist/workspace/index.js" + }, + "./worktree": { + "types": "./dist/worktree/index.d.ts", + "default": "./dist/worktree/index.js" } }, "scripts": { diff --git a/packages/cellix/local-dev/src/dotenv.ts b/packages/cellix/local-dev/src/files/dotenv.ts similarity index 100% rename from packages/cellix/local-dev/src/dotenv.ts rename to packages/cellix/local-dev/src/files/dotenv.ts diff --git a/packages/cellix/local-dev/src/files/index.ts b/packages/cellix/local-dev/src/files/index.ts new file mode 100644 index 000000000..6a3484aee --- /dev/null +++ b/packages/cellix/local-dev/src/files/index.ts @@ -0,0 +1,2 @@ +export { type DotEnvValues, readDotEnv } from './dotenv.ts'; +export { readJsonFile, type SyncJsonFileOptions, syncJsonFile, writeJsonFile } from './json.ts'; diff --git a/packages/cellix/local-dev/src/json-files.ts b/packages/cellix/local-dev/src/files/json.ts similarity index 100% rename from packages/cellix/local-dev/src/json-files.ts rename to packages/cellix/local-dev/src/files/json.ts diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 923db1322..499bb9ee0 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -1,7 +1,10 @@ +import { EventEmitter } from 'node:events'; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { + AzureFunctionsDevRunner, + AzuriteDevRunner, applyWorktreeSuffix, buildAzuriteConnectionString, buildPortlessUrl, @@ -10,12 +13,17 @@ import { getMongoPort, getWorktreePortOffset, hostnameFromUrl, + NodeDevRunner, PORTLESS_PORT, + type RunnerSpawn, readDotEnv, replaceUrlPort, resolveWorkspaceRoot, sanitizeWorktreeHostnameLabel, syncJsonFile, + ViteDevRunner, + WorktreeJsonFileSync, + WorktreeSettings, } from '@cellix/local-dev'; import { describe, expect, it } from 'vitest'; @@ -131,4 +139,243 @@ describe('@cellix/local-dev', () => { }, }); }); + + it('starts runner objects with inherited stdio and caller-provided environment', () => { + const calls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = []; + const spawn: RunnerSpawn = (command, args, options) => { + calls.push({ + command, + args, + ...(options.env ? { env: options.env } : {}), + }); + return new EventEmitter() as ReturnType; + }; + + new ViteDevRunner({ + env: { PORT: '4444', HOST: '127.0.0.1' }, + spawn, + }).start(); + new NodeDevRunner({ + env: { PORT: '50000' }, + spawn, + }).start(); + + expect(calls).toEqual([ + { + command: 'vite', + args: ['--host', '127.0.0.1', '--port', '4444'], + env: { PORT: '4444', HOST: '127.0.0.1' }, + }, + { + command: 'node', + args: ['src/index.ts'], + env: { PORT: '50000' }, + }, + ]); + }); + + it('applies worktree transforms to arbitrary env settings', () => { + const env = new WorktreeSettings({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + settings: { + BASE_URL: 'https://mock-auth.ownercommunity.localhost:1355', + API_URL: 'https://data-access.ownercommunity.localhost:1355/api/graphql', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + PORT: '50000', + }, + }).toEnv(); + + expect(env).toMatchObject({ + BASE_URL: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355', + API_URL: 'https://data-access.ownercommunity.jason-feature-123.localhost:1355/api/graphql', + COSMOSDB_CONNECTION_STRING: expect.not.stringMatching(':50000/ocom'), + PORT: String(getMongoPort('Jason/Feature 123')), + }); + }); + + it('does not apply worktree transforms when CELLIX_WORKTREE disables them', () => { + const env = new WorktreeSettings({ + env: { CELLIX_WORKTREE: '0', WORKTREE_NAME: 'Jason/Feature 123' }, + settings: { + BASE_URL: 'https://mock-auth.ownercommunity.localhost:1355', + PORT: '50000', + }, + }).toEnv(); + + expect(env).toMatchObject({ + BASE_URL: 'https://mock-auth.ownercommunity.localhost:1355', + PORT: '50000', + }); + }); + + it('starts worktree-aware Vite and Node runners with transformed settings', () => { + const calls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = []; + const spawn: RunnerSpawn = (command, args, options) => { + calls.push({ + command, + args, + ...(options.env ? { env: options.env } : {}), + }); + return new EventEmitter() as ReturnType; + }; + + new ViteDevRunner({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + settings: { + VITE_COMMON_API_ENDPOINT: 'https://data-access.ownercommunity.localhost:1355/api/graphql', + }, + spawn, + }).start(); + new NodeDevRunner({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + settings: { + PORT: '50000', + }, + spawn, + }).start(); + + expect(calls[0]).toMatchObject({ + command: 'vite', + env: { + VITE_COMMON_API_ENDPOINT: 'https://data-access.ownercommunity.jason-feature-123.localhost:1355/api/graphql', + }, + }); + expect(calls[1]).toMatchObject({ + command: 'node', + args: ['src/index.ts'], + env: { + PORT: String(getMongoPort('Jason/Feature 123')), + }, + }); + }); + + it('derives worktree-aware Azurite options generically', () => { + const workspaceRoot = createWorkspaceFixture(); + const options = new AzuriteDevRunner({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + workspaceRoot, + }).resolveOptions(); + + expect(options).toMatchObject({ + blobPort: getAzuritePorts('Jason/Feature 123').blob, + queuePort: getAzuritePorts('Jason/Feature 123').queue, + tablePort: getAzuritePorts('Jason/Feature 123').table, + blobLocation: path.join(workspaceRoot, '__blobstorage__-Jason/Feature 123'), + }); + }); + + it('syncs json settings with generic worktree URL, Mongo, and Azurite transforms', () => { + const workspaceRoot = createWorkspaceFixture(); + const sourcePath = path.join(workspaceRoot, 'fixtures', 'settings.e2e.json'); + const targetPath = path.join(workspaceRoot, 'fixtures', 'deploy', 'local.settings.json'); + writeFileSync( + sourcePath, + JSON.stringify({ + Values: { + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + }, + }), + ); + + new WorktreeJsonFileSync({ + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + sourcePath, + targetPath, + values: { + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + }, + azuriteConnectionStringKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], + }).sync(); + + const settings = JSON.parse(readFileSync(targetPath, 'utf8')); + expect(settings.Values).toMatchObject({ + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', + AZURE_STORAGE_CONNECTION_STRING: expect.stringContaining('BlobEndpoint=http://127.0.0.1:'), + AzureWebJobsStorage: expect.stringContaining('QueueEndpoint=http://127.0.0.1:'), + }); + expect(settings.Values.COSMOSDB_CONNECTION_STRING).not.toBe('mongodb://127.0.0.1:50000/ocom'); + }); + + it('syncs json settings without worktree transforms when CELLIX_WORKTREE disables them', () => { + const workspaceRoot = createWorkspaceFixture(); + const sourcePath = path.join(workspaceRoot, 'fixtures', 'settings.local.json'); + const targetPath = path.join(workspaceRoot, 'fixtures', 'deploy', 'local.settings.json'); + writeFileSync( + sourcePath, + JSON.stringify({ + Values: { + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + }, + }), + ); + + new WorktreeJsonFileSync({ + env: { CELLIX_WORKTREE: '0', WORKTREE_NAME: 'Jason/Feature 123' }, + sourcePath, + targetPath, + values: { + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + }, + azuriteConnectionStringKeys: ['AzureWebJobsStorage'], + }).sync(); + + const settings = JSON.parse(readFileSync(targetPath, 'utf8')); + expect(settings.Values).toMatchObject({ + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + }); + expect(settings.Values.AzureWebJobsStorage).toBeUndefined(); + }); + + it('prepares Azure Functions local settings before starting func', () => { + const workspaceRoot = createWorkspaceFixture(); + const appDir = path.join(workspaceRoot, 'fixtures', 'api'); + const calls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = []; + const spawn: RunnerSpawn = (command, args, options) => { + calls.push({ + command, + args, + ...(options.env ? { env: options.env } : {}), + }); + return new EventEmitter() as ReturnType; + }; + mkdirSync(appDir, { recursive: true }); + writeFileSync( + path.join(appDir, 'local-settings.e2e.json'), + JSON.stringify({ + Values: { + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + }, + }), + ); + + new AzureFunctionsDevRunner({ + env: { E2E: 'true', PORT: '7071', WORKTREE_NAME: 'Jason/Feature 123' }, + localSettings: { + appDir, + e2eValues: { + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + }, + azuriteConnectionStringKeys: ['AzureWebJobsStorage'], + }, + spawn, + }).start(); + + const settings = JSON.parse(readFileSync(path.join(appDir, 'deploy', 'local.settings.json'), 'utf8')); + expect(settings.Values).toMatchObject({ + ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', + AzureWebJobsStorage: expect.stringContaining('BlobEndpoint=http://127.0.0.1:'), + }); + expect(settings.Values.COSMOSDB_CONNECTION_STRING).not.toBe('mongodb://127.0.0.1:50000/ocom'); + expect(calls[0]).toMatchObject({ + command: 'func', + args: ['start', '--typescript', '--script-root', 'deploy/', '--port', '7071', '--cors', '*'], + }); + }); }); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index c30c35820..46e783511 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,25 +1,38 @@ -export { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; export { type DotEnvValues, readDotEnv, -} from './dotenv.ts'; +} from './files/dotenv.ts'; export { readJsonFile, type SyncJsonFileOptions, syncJsonFile, writeJsonFile, -} from './json-files.ts'; +} from './files/json.ts'; +export { forwardChildExit, isGracefulInterruptExit } from './process/index.ts'; export { type AzureFunctionsDevOptions, + AzureFunctionsDevRunner, + AzureFunctionsLocalSettings, + type AzureFunctionsLocalSettingsOptions, type AzuriteDevOptions, + AzuriteDevRunner, + DocusaurusDevRunner, + type EnvRunnerOptions, + type NodeDevOptions, + NodeDevRunner, + type ResolvedAzuriteOptions, type RunnerOptions, + type RunnerSpawn, runAzureFunctionsDev, runAzuriteDev, runDocusaurusDev, + runNodeDev, runTsxDev, runViteDev, type TsxRunnerOptions, -} from './runners.ts'; + type ViteDevOptions, + ViteDevRunner, +} from './runners/index.ts'; export { applyWorktreeSuffix, buildPortlessUrl, @@ -27,13 +40,20 @@ export { PORTLESS_PORT, replaceUrlPort, sanitizeWorktreeHostnameLabel, -} from './urls.ts'; -export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite.ts'; -export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace.ts'; +} from './urls/index.ts'; +export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite/index.ts'; +export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace/index.ts'; +export { + WorktreeJsonFileSync, + type WorktreeJsonFileSyncOptions, + type WorktreeMode, + WorktreeSettings, + type WorktreeSettingsOptions, +} from './worktree/index.ts'; export { type AzuritePorts, buildAzuriteConnectionString, getAzuritePorts, getMongoPort, getWorktreePortOffset, -} from './worktree-ports.ts'; +} from './worktree/ports.ts'; diff --git a/packages/cellix/local-dev/src/dev-process.ts b/packages/cellix/local-dev/src/process/index.ts similarity index 100% rename from packages/cellix/local-dev/src/dev-process.ts rename to packages/cellix/local-dev/src/process/index.ts diff --git a/packages/cellix/local-dev/src/runners.ts b/packages/cellix/local-dev/src/runners.ts deleted file mode 100644 index 777b6e4eb..000000000 --- a/packages/cellix/local-dev/src/runners.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { type ChildProcess, spawn } from 'node:child_process'; -import os from 'node:os'; -import path from 'node:path'; -import { forwardChildExit, isGracefulInterruptExit } from './dev-process.ts'; -import { buildViteArgs } from './vite.ts'; - -type RunnerEnv = NodeJS.ProcessEnv & { - HOST?: string; - NODE_OPTIONS?: string; - PORT?: string; - PORTLESS_CA_PATH?: string; -}; - -export interface RunnerOptions { - /** Environment to pass to the spawned process. Defaults to `process.env`. */ - env?: NodeJS.ProcessEnv; -} - -export interface TsxRunnerOptions extends RunnerOptions { - /** TSX entrypoint to execute. Defaults to `src/index.ts`. */ - entry?: string; -} - -export interface AzureFunctionsDevOptions extends RunnerOptions { - /** Functions host port. Defaults to `env.PORT`; required after resolution. */ - port?: string; - /** Azure Functions script root. Defaults to `deploy/`. */ - scriptRoot?: string; - /** CORS value passed to the Functions host. Defaults to `*`. */ - cors?: string; - /** Whether to pass `--typescript`. Defaults to `true`. */ - typescript?: boolean; -} - -export interface AzuriteDevOptions extends RunnerOptions { - /** Blob service port. */ - blobPort: number; - /** Queue service port. */ - queuePort: number; - /** Table service port. */ - tablePort: number; - /** Blob storage working directory. */ - blobLocation: string; - /** Queue storage working directory. */ - queueLocation: string; - /** Table storage working directory. */ - tableLocation: string; - /** Whether Azurite should run quietly. Defaults to `true`. */ - silent?: boolean; -} - -function spawnInherited(command: string, args: string[], options: { env?: NodeJS.ProcessEnv } = {}): ChildProcess { - return spawn(command, args, { - stdio: 'inherit', - env: options.env, - }); -} - -/** - * Starts a Vite dev process using the caller-provided environment. - * - * @param options - Optional environment overrides. `HOST`, `PORT`, `E2E`, and - * `E2E_VITE_MODE` are interpreted by `buildViteArgs`. - * @returns The spawned Vite child process. - */ -export function runViteDev(options: RunnerOptions = {}): ChildProcess { - const env = (options.env ?? process.env) as RunnerEnv; - const child = spawnInherited( - 'vite', - buildViteArgs({ - ...(env.HOST ? { host: env.HOST } : {}), - ...(env.PORT ? { port: env.PORT } : {}), - env, - }), - { env }, - ); - forwardChildExit(child); - return child; -} - -/** - * Starts the Docusaurus dev server with the shared local-dev defaults. - * - * @param options - Optional environment overrides. `PORT` controls the internal - * Docusaurus listener and defaults to `3001`. - * @returns The spawned Docusaurus child process. - */ -export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { - const env = (options.env ?? process.env) as RunnerEnv; - const child = spawnInherited('docusaurus', ['start', '--port', env.PORT ?? '3001', '--host', '127.0.0.1', '--no-open'], { env }); - forwardChildExit(child); - return child; -} - -/** - * Starts an Azure Functions dev process using caller-supplied runtime - * configuration and environment variables. - * - * @param options - Functions host options plus optional environment overrides. - * `PORT` must be provided directly or through the environment. - * @returns The spawned Functions host child process. - * @throws When no port can be resolved. - */ -export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): ChildProcess { - const env = (options.env ?? process.env) as RunnerEnv; - const envPort = options.port ?? env.PORT; - - if (!envPort) { - throw new Error('[local-dev] PORT environment variable is not set. Start this command through portless.'); - } - - const childEnv: NodeJS.ProcessEnv = { - ...env, - NODE_EXTRA_CA_CERTS: env.PORTLESS_CA_PATH ?? path.join(os.homedir(), '.portless', 'ca.pem'), - NODE_OPTIONS: `${env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), - }; - - const args = ['start']; - if (options.typescript ?? true) { - args.push('--typescript'); - } - - args.push('--script-root', options.scriptRoot ?? 'deploy/', '--port', envPort, '--cors', options.cors ?? '*'); - - const child = spawnInherited('func', args, { env: childEnv }); - forwardChildExit(child); - return child; -} - -/** - * Starts a TSX-backed dev process using the caller-provided entrypoint and - * environment. - * - * @param options - Optional entrypoint and environment overrides. - * @returns The spawned TSX child process. - */ -export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { - const env = options.env ?? process.env; - const child = spawnInherited('tsx', [options.entry ?? 'src/index.ts'], { env }); - forwardChildExit(child); - return child; -} - -/** - * Starts the three Azurite worker processes using caller-supplied ports and - * storage paths. - * - * @param options - Explicit Azurite ports, storage locations, and optional - * environment. - * @returns The spawned blob, queue, and table child processes. - */ -export function runAzuriteDev(options: AzuriteDevOptions): ChildProcess[] { - const procSpecs: Array<[string, string[]]> = [ - ['azurite-blob', [...((options.silent ?? true) ? ['--silent'] : []), '--blobPort', String(options.blobPort), '--location', path.resolve(options.blobLocation)]], - ['azurite-queue', [...((options.silent ?? true) ? ['--silent'] : []), '--queuePort', String(options.queuePort), '--location', path.resolve(options.queueLocation)]], - ['azurite-table', [...((options.silent ?? true) ? ['--silent'] : []), '--tablePort', String(options.tablePort), '--location', path.resolve(options.tableLocation)]], - ]; - - const procs = procSpecs.map(([command, args]) => { - const proc = spawnInherited(command, args, options.env ? { env: options.env } : {}); - proc.on('error', (error) => { - console.error(`[azurite] failed to start ${command}: ${error.message}`); - for (const runningProc of procs) { - runningProc.kill(); - } - process.exit(1); - }); - return proc; - }); - - console.log(`[azurite] started (blob=${options.blobPort}, queue=${options.queuePort}, table=${options.tablePort})`); - - let exited = 0; - for (const proc of procs) { - proc.on('exit', (code, signal) => { - if (isGracefulInterruptExit(signal, code)) { - if (++exited === procs.length) { - process.exit(0); - } - return; - } - - console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); - for (const runningProc of procs) { - runningProc.kill(); - } - process.exit(code ?? 1); - }); - } - - process.on('SIGINT', () => { - for (const proc of procs) { - proc.kill('SIGINT'); - } - }); - process.on('SIGTERM', () => { - for (const proc of procs) { - proc.kill('SIGTERM'); - } - }); - - return procs; -} diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts new file mode 100644 index 000000000..6752ed3cd --- /dev/null +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -0,0 +1,78 @@ +import path from 'node:path'; +import { WorktreeJsonFileSync } from '../worktree/json-file-sync.ts'; +import type { SettingsRecord, WorktreeMode } from '../worktree/types.ts'; + +type LocalSettingsEnv = NodeJS.ProcessEnv & { + E2E?: string; +}; + +function isE2E(env: NodeJS.ProcessEnv): boolean { + return ['1', 'true', 'yes'].includes(((env as LocalSettingsEnv).E2E ?? '').toLowerCase()); +} + +export interface AzureFunctionsLocalSettingsOptions { + /** App directory containing the source local settings files. Defaults to `process.cwd()`. */ + appDir?: string; + /** Functions script root. Defaults to `deploy/`. */ + scriptRoot?: string; + /** Base environment. Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; + /** Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, then auto mode. */ + worktree?: WorktreeMode; + /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ + worktreeName?: string; + /** Source settings file for normal dev mode. Defaults to `/local.settings.json`. */ + sourcePath?: string; + /** Source settings file for E2E mode. Defaults to `/local-settings.e2e.json`. */ + e2eSourcePath?: string; + /** Target settings file. Defaults to `//local.settings.json`. */ + targetPath?: string; + /** Values merged into every mode before worktree transforms. */ + values?: SettingsRecord; + /** Values merged only when `E2E` is truthy. */ + e2eValues?: SettingsRecord; + /** Keys that should receive a worktree-scoped Azurite connection string. */ + azuriteConnectionStringKeys?: string[]; +} + +/** + * Prepares Azure Functions `local.settings.json` before `func start`. + * + * Azure Functions reads `local.settings.json` from the script root. This helper + * copies the mode-appropriate source file into that location and applies the + * generic worktree URL, Mongo, and Azurite transforms. Normal dev reads + * `local.settings.json` when present; E2E mode reads `local-settings.e2e.json` + * and fails if that source is missing. + */ +export class AzureFunctionsLocalSettings { + private readonly options: AzureFunctionsLocalSettingsOptions; + + public constructor(options: AzureFunctionsLocalSettingsOptions = {}) { + this.options = options; + } + + /** + * Syncs the mode-appropriate local settings file into the Functions script + * root. + * + * @throws When E2E mode is active and its source file is missing. + */ + public sync(): void { + const { appDir = process.cwd(), scriptRoot = 'deploy/', env = process.env } = this.options; + const e2e = isE2E(env); + + new WorktreeJsonFileSync({ + env, + ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), + ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), + sourcePath: e2e ? (this.options.e2eSourcePath ?? path.join(appDir, 'local-settings.e2e.json')) : (this.options.sourcePath ?? path.join(appDir, 'local.settings.json')), + targetPath: this.options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'), + skipIfMissing: !e2e, + values: { + ...(this.options.values ?? {}), + ...(e2e ? (this.options.e2eValues ?? {}) : {}), + }, + ...(this.options.azuriteConnectionStringKeys ? { azuriteConnectionStringKeys: this.options.azuriteConnectionStringKeys } : {}), + }).sync(); + } +} diff --git a/packages/cellix/local-dev/src/runners/azure-functions.ts b/packages/cellix/local-dev/src/runners/azure-functions.ts new file mode 100644 index 000000000..246c7e651 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/azure-functions.ts @@ -0,0 +1,123 @@ +import type { ChildProcess } from 'node:child_process'; +import os from 'node:os'; +import path from 'node:path'; +import { forwardChildExit } from '../process/index.ts'; +import { WorktreeSettings } from '../worktree/settings.ts'; +import type { WorktreeMode } from '../worktree/types.ts'; +import { resolveWorktreeName } from '../worktree/worktree-name.ts'; +import { AzureFunctionsLocalSettings, type AzureFunctionsLocalSettingsOptions } from './azure-functions-local-settings.ts'; +import { spawnInherited } from './spawn.ts'; +import type { EnvRunnerOptions } from './types.ts'; + +type RunnerEnv = NodeJS.ProcessEnv & { + NODE_OPTIONS?: string; + PORT?: string; + PORTLESS_CA_PATH?: string; +}; + +export interface AzureFunctionsDevOptions extends EnvRunnerOptions { + /** + * Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, + * then auto mode. The api is the only app that toggles this: its plain `dev` + * sets `CELLIX_WORKTREE=0` so settings sync without worktree scoping. + */ + worktree?: WorktreeMode; + /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ + worktreeName?: string; + /** Functions host port. Defaults to `env.PORT`; required after resolution. */ + port?: string; + /** Azure Functions script root. Defaults to `deploy/`. */ + scriptRoot?: string; + /** CORS value passed to the Functions host. Defaults to `*`. */ + cors?: string; + /** Whether to pass `--typescript`. Defaults to `true`. */ + typescript?: boolean; + /** Local settings to sync into the script root before starting Functions. */ + localSettings?: AzureFunctionsLocalSettingsOptions; +} + +/** + * Resolves the worktree decision once so the env transform and local-settings + * sync agree without each layer re-reading the environment. + * + * @param options - Worktree mode and name (falling back to the environment). + * @returns `{ worktree: false }` when transforms are disabled, otherwise + * `{ worktree: true, worktreeName }`. + */ +function resolveWorktreeContext(options: { env?: NodeJS.ProcessEnv; worktree?: WorktreeMode; worktreeName?: string }): { worktree: boolean; worktreeName?: string } { + const worktreeName = resolveWorktreeName(options); + return worktreeName === undefined ? { worktree: false } : { worktree: true, worktreeName }; +} + +/** + * Prepares Azure Functions local settings and starts the host with a + * worktree-aware environment. + * + * The runner injects local Portless certificate settings so Functions can call + * other local HTTPS services during development. + */ +export class AzureFunctionsDevRunner { + private readonly options: AzureFunctionsDevOptions; + + public constructor(options: AzureFunctionsDevOptions = {}) { + this.options = options; + } + + /** + * Optionally syncs Functions local settings, then spawns `func start` with + * inherited stdio. + * + * @returns The spawned Functions host process. + * @throws When no port is supplied through `options.port` or `env.PORT`. + */ + public start(): ChildProcess { + const worktreeContext = resolveWorktreeContext(this.options); + const env = new WorktreeSettings({ ...this.options, ...worktreeContext }).toEnv() as RunnerEnv; + + const localSettings = this.options.localSettings; + if (localSettings) { + const scriptRoot = localSettings.scriptRoot ?? this.options.scriptRoot; + new AzureFunctionsLocalSettings({ + ...localSettings, + ...worktreeContext, + env, + ...(scriptRoot ? { scriptRoot } : {}), + }).sync(); + } + + const envPort = this.options.port ?? env.PORT; + if (!envPort) { + throw new Error('[local-dev] PORT environment variable is not set. Start this command through portless.'); + } + + const childEnv: NodeJS.ProcessEnv = { + ...env, + NODE_EXTRA_CA_CERTS: env.PORTLESS_CA_PATH ?? path.join(os.homedir(), '.portless', 'ca.pem'), + NODE_OPTIONS: `${env.NODE_OPTIONS ?? ''} --use-system-ca`.trim(), + }; + + const args = ['start']; + if (this.options.typescript ?? true) { + args.push('--typescript'); + } + args.push('--script-root', this.options.scriptRoot ?? 'deploy/', '--port', envPort, '--cors', this.options.cors ?? '*'); + + const child = spawnInherited('func', args, { + env: childEnv, + ...(this.options.spawn ? { spawn: this.options.spawn } : {}), + }); + forwardChildExit(child); + return child; + } +} + +/** + * Starts an Azure Functions dev process. + * + * @param options - Functions host options, worktree context, env, and spawn + * override. + * @returns The spawned Functions host process. + */ +export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): ChildProcess { + return new AzureFunctionsDevRunner(options).start(); +} diff --git a/packages/cellix/local-dev/src/runners/azurite.ts b/packages/cellix/local-dev/src/runners/azurite.ts new file mode 100644 index 000000000..6ed7736d9 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/azurite.ts @@ -0,0 +1,153 @@ +import type { ChildProcess } from 'node:child_process'; +import path from 'node:path'; +import { isGracefulInterruptExit } from '../process/index.ts'; +import { resolveWorkspaceRoot } from '../workspace/index.ts'; +import { getAzuritePorts } from '../worktree/ports.ts'; +import { resolveWorktreeName } from '../worktree/worktree-name.ts'; +import { spawnInherited } from './spawn.ts'; +import type { RunnerOptions } from './types.ts'; + +export interface AzuriteDevOptions extends RunnerOptions { + /** Workspace root used for storage directories. Defaults to auto-discovery. */ + workspaceRoot?: string; + /** Storage directory names before worktree suffixing. */ + storageDirectories?: { + blob?: string; + queue?: string; + table?: string; + }; + /** Whether Azurite should run quietly. Defaults to `true`. */ + silent?: boolean; +} + +/** + * Resolved Azurite ports and storage locations after worktree scoping. + */ +export interface ResolvedAzuriteOptions { + /** Blob service port. */ + blobPort: number; + /** Queue service port. */ + queuePort: number; + /** Table service port. */ + tablePort: number; + /** Blob storage working directory. */ + blobLocation: string; + /** Queue storage working directory. */ + queueLocation: string; + /** Table storage working directory. */ + tableLocation: string; +} + +/** + * Starts the three Azurite worker processes on worktree-scoped ports and + * storage directories. + * + * When no worktree is active the runner falls back to Azurite's default ports + * and unsuffixed storage directories. All three services inherit stdio; if one + * exits unexpectedly the runner terminates the others and forwards a non-zero + * exit code. + */ +export class AzuriteDevRunner { + private readonly options: AzuriteDevOptions; + + public constructor(options: AzuriteDevOptions = {}) { + this.options = options; + } + + /** + * Resolves the worktree-scoped Azurite ports and storage locations. + * + * @returns Ports and storage directories for the active worktree, or the + * defaults when no worktree is active. + */ + public resolveOptions(): ResolvedAzuriteOptions { + const worktreeName = resolveWorktreeName(this.options); + const workspaceRoot = this.options.workspaceRoot ?? resolveWorkspaceRoot(); + const ports = getAzuritePorts(worktreeName); + const storageSuffix = worktreeName ? `-${worktreeName}` : ''; + const directories = this.options.storageDirectories ?? {}; + + return { + blobPort: ports.blob, + blobLocation: path.join(workspaceRoot, `${directories.blob ?? '__blobstorage__'}${storageSuffix}`), + queuePort: ports.queue, + queueLocation: path.join(workspaceRoot, `${directories.queue ?? '__queuestorage__'}${storageSuffix}`), + tablePort: ports.table, + tableLocation: path.join(workspaceRoot, `${directories.table ?? '__tablestorage__'}${storageSuffix}`), + }; + } + + /** + * Spawns blob, queue, and table Azurite processes. + * + * @returns The spawned Azurite child processes. + */ + public start(): ChildProcess[] { + const resolved = this.resolveOptions(); + const silent = this.options.silent ?? true; + const procSpecs: Array<[string, string[]]> = [ + ['azurite-blob', [...(silent ? ['--silent'] : []), '--blobPort', String(resolved.blobPort), '--location', path.resolve(resolved.blobLocation)]], + ['azurite-queue', [...(silent ? ['--silent'] : []), '--queuePort', String(resolved.queuePort), '--location', path.resolve(resolved.queueLocation)]], + ['azurite-table', [...(silent ? ['--silent'] : []), '--tablePort', String(resolved.tablePort), '--location', path.resolve(resolved.tableLocation)]], + ]; + + const procs = procSpecs.map(([command, args]) => { + const proc = spawnInherited(command, args, { + ...(this.options.env ? { env: this.options.env } : {}), + ...(this.options.spawn ? { spawn: this.options.spawn } : {}), + }); + proc.on('error', (error) => { + console.error(`[azurite] failed to start ${command}: ${error.message}`); + for (const runningProc of procs) { + runningProc.kill(); + } + process.exit(1); + }); + return proc; + }); + + console.log(`[azurite] started (blob=${resolved.blobPort}, queue=${resolved.queuePort}, table=${resolved.tablePort})`); + + let exited = 0; + for (const proc of procs) { + proc.on('exit', (code, signal) => { + if (isGracefulInterruptExit(signal, code)) { + if (++exited === procs.length) { + process.exit(0); + } + return; + } + + console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); + for (const runningProc of procs) { + runningProc.kill(); + } + process.exit(code ?? 1); + }); + } + + process.on('SIGINT', () => { + for (const proc of procs) { + proc.kill('SIGINT'); + } + }); + process.on('SIGTERM', () => { + for (const proc of procs) { + proc.kill('SIGTERM'); + } + }); + + return procs; + } +} + +/** + * Starts Azurite blob, queue, and table services on worktree-scoped ports. + * + * @param options - Optional workspace root, storage directories, worktree + * context, env, and spawn override. + * @returns The spawned Azurite child processes. + */ +export function runAzuriteDev(options: AzuriteDevOptions = {}): ChildProcess[] { + return new AzuriteDevRunner(options).start(); +} diff --git a/packages/cellix/local-dev/src/runners/docusaurus.ts b/packages/cellix/local-dev/src/runners/docusaurus.ts new file mode 100644 index 000000000..dcbf1eb60 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/docusaurus.ts @@ -0,0 +1,47 @@ +import type { ChildProcess } from 'node:child_process'; +import { forwardChildExit } from '../process/index.ts'; +import { spawnInherited } from './spawn.ts'; +import type { RunnerOptions } from './types.ts'; + +type RunnerEnv = NodeJS.ProcessEnv & { + PORT?: string; +}; + +/** + * Starts the Docusaurus dev server with the shared local-dev defaults. + * + * The server binds to `127.0.0.1`, uses `env.PORT` or `3001`, and does not + * auto-open a browser. + */ +export class DocusaurusDevRunner { + private readonly options: RunnerOptions; + + public constructor(options: RunnerOptions = {}) { + this.options = options; + } + + /** + * Spawns the Docusaurus dev server with inherited stdio. + * + * @returns The spawned child process. + */ + public start(): ChildProcess { + const env = (this.options.env ?? process.env) as RunnerEnv; + const child = spawnInherited('docusaurus', ['start', '--port', env.PORT ?? '3001', '--host', '127.0.0.1', '--no-open'], { + env, + ...(this.options.spawn ? { spawn: this.options.spawn } : {}), + }); + forwardChildExit(child); + return child; + } +} + +/** + * Starts the Docusaurus dev server. + * + * @param options - Optional env and spawn override. + * @returns The spawned child process. + */ +export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { + return new DocusaurusDevRunner(options).start(); +} diff --git a/packages/cellix/local-dev/src/runners/index.ts b/packages/cellix/local-dev/src/runners/index.ts new file mode 100644 index 000000000..4a360b9d0 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/index.ts @@ -0,0 +1,7 @@ +export { type AzureFunctionsDevOptions, AzureFunctionsDevRunner, runAzureFunctionsDev } from './azure-functions.ts'; +export { AzureFunctionsLocalSettings, type AzureFunctionsLocalSettingsOptions } from './azure-functions-local-settings.ts'; +export { type AzuriteDevOptions, AzuriteDevRunner, type ResolvedAzuriteOptions, runAzuriteDev } from './azurite.ts'; +export { DocusaurusDevRunner, runDocusaurusDev } from './docusaurus.ts'; +export { type NodeDevOptions, NodeDevRunner, runNodeDev, runTsxDev, type TsxRunnerOptions } from './node.ts'; +export type { EnvRunnerOptions, RunnerOptions, RunnerSpawn } from './types.ts'; +export { runViteDev, type ViteDevOptions, ViteDevRunner } from './vite.ts'; diff --git a/packages/cellix/local-dev/src/runners/node.ts b/packages/cellix/local-dev/src/runners/node.ts new file mode 100644 index 000000000..4f8d297df --- /dev/null +++ b/packages/cellix/local-dev/src/runners/node.ts @@ -0,0 +1,71 @@ +import type { ChildProcess } from 'node:child_process'; +import { forwardChildExit } from '../process/index.ts'; +import { WorktreeSettings } from '../worktree/settings.ts'; +import { spawnInherited } from './spawn.ts'; +import type { EnvRunnerOptions } from './types.ts'; + +export interface NodeDevOptions extends EnvRunnerOptions { + /** Node entrypoint to execute. Defaults to `src/index.ts`. */ + entry?: string; + /** Additional Node CLI arguments passed before the entrypoint. */ + nodeArgs?: string[]; +} + +/** + * Starts a Node-backed dev process. On the supported workspace Node version, + * this can execute TypeScript entrypoints without the TSX runtime. + * + * @example + * ```ts + * new NodeDevRunner({ env, entry: 'src/index.ts' }).start(); + * ``` + */ +export class NodeDevRunner { + private readonly options: NodeDevOptions; + + public constructor(options: NodeDevOptions = {}) { + this.options = options; + } + + /** + * Spawns the configured Node process with inherited stdio. + * + * @returns The spawned child process. + */ + public start(): ChildProcess { + const env = new WorktreeSettings(this.options).toEnv(); + const child = spawnInherited('node', [...(this.options.nodeArgs ?? []), this.options.entry ?? 'src/index.ts'], { + env, + ...(this.options.spawn ? { spawn: this.options.spawn } : {}), + }); + forwardChildExit(child); + return child; + } +} + +/** + * Starts a Node-backed dev process. + * + * @param options - Optional entrypoint, Node arguments, env, and spawn + * override. + * @returns The spawned child process. + */ +export function runNodeDev(options: NodeDevOptions = {}): ChildProcess { + return new NodeDevRunner(options).start(); +} + +/** + * @deprecated Use `NodeDevRunner` or `runNodeDev`. TSX is no longer required + * for local dev entrypoints on the supported Node runtime. + */ +export type TsxRunnerOptions = NodeDevOptions; + +/** + * @deprecated Use `runNodeDev`. + * + * @param options - Node dev options preserved for compatibility. + * @returns The spawned child process. + */ +export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { + return runNodeDev(options); +} diff --git a/packages/cellix/local-dev/src/runners/spawn.ts b/packages/cellix/local-dev/src/runners/spawn.ts new file mode 100644 index 000000000..b059987f3 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/spawn.ts @@ -0,0 +1,17 @@ +import { type ChildProcess, spawn } from 'node:child_process'; +import type { RunnerOptions } from './types.ts'; + +/** + * Spawns a command with inherited stdio and the caller-provided environment. + * + * @param command - Executable name. + * @param args - Command-line arguments. + * @param options - Optional env and spawn override. + * @returns The spawned child process. + */ +export function spawnInherited(command: string, args: string[], options: RunnerOptions = {}): ChildProcess { + return (options.spawn ?? spawn)(command, args, { + stdio: 'inherit', + env: options.env, + }); +} diff --git a/packages/cellix/local-dev/src/runners/types.ts b/packages/cellix/local-dev/src/runners/types.ts new file mode 100644 index 000000000..821a68397 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/types.ts @@ -0,0 +1,34 @@ +import type { ChildProcess, SpawnOptions } from 'node:child_process'; +import type { SettingsRecord } from '../worktree/types.ts'; + +/** + * Spawn function used by dev runners. + * + * Consumers usually rely on the default `child_process.spawn`; tests and + * advanced wrappers can inject this to observe or customize process startup. + */ +export type RunnerSpawn = (command: string, args: string[], options: SpawnOptions) => ChildProcess; + +/** + * Common process options shared by local-dev runner objects. + */ +export interface RunnerOptions { + /** Environment to pass to the spawned process. Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; + /** Process spawner used by tests and advanced consumers. Defaults to `child_process.spawn`. */ + spawn?: RunnerSpawn; +} + +/** + * Runner options for processes whose environment is built from caller-provided + * settings. + * + * Settings are merged over `env` and worktree-transformed when a worktree is + * active (auto-detected from `WORKTREE_NAME`). These runners are invoked only by + * the `dev:worktree` scripts, so they have no enable/disable toggle: plain `dev` + * runs the underlying tool directly without a wrapper. + */ +export interface EnvRunnerOptions extends RunnerOptions { + /** Settings merged over `env` before worktree transforms are applied. */ + settings?: SettingsRecord; +} diff --git a/packages/cellix/local-dev/src/runners/vite.ts b/packages/cellix/local-dev/src/runners/vite.ts new file mode 100644 index 000000000..a4a72c1c5 --- /dev/null +++ b/packages/cellix/local-dev/src/runners/vite.ts @@ -0,0 +1,58 @@ +import type { ChildProcess } from 'node:child_process'; +import { forwardChildExit } from '../process/index.ts'; +import { buildViteArgs } from '../vite/index.ts'; +import { WorktreeSettings } from '../worktree/settings.ts'; +import { spawnInherited } from './spawn.ts'; +import type { EnvRunnerOptions } from './types.ts'; + +export type ViteDevOptions = EnvRunnerOptions; + +/** + * Starts a Vite dev process using the caller-provided environment. + * + * @example + * ```ts + * new ViteDevRunner({ env }).start(); + * ``` + */ +export class ViteDevRunner { + private readonly options: ViteDevOptions; + + public constructor(options: ViteDevOptions = {}) { + this.options = options; + } + + /** + * Spawns Vite with host, port, and mode derived from the supplied env. + * + * @returns The spawned Vite child process. + */ + public start(): ChildProcess { + const env = new WorktreeSettings(this.options).toEnv(); + const { HOST: host, PORT: port } = env; + const child = spawnInherited( + 'vite', + buildViteArgs({ + ...(host ? { host } : {}), + ...(port ? { port } : {}), + env, + }), + { + env, + ...(this.options.spawn ? { spawn: this.options.spawn } : {}), + }, + ); + forwardChildExit(child); + return child; + } +} + +/** + * Starts a Vite dev process. + * + * @param options - Optional env and spawn override. + * @returns The spawned child process. + */ +export function runViteDev(options: ViteDevOptions = {}): ChildProcess { + return new ViteDevRunner(options).start(); +} diff --git a/packages/cellix/local-dev/src/urls.ts b/packages/cellix/local-dev/src/urls/index.ts similarity index 100% rename from packages/cellix/local-dev/src/urls.ts rename to packages/cellix/local-dev/src/urls/index.ts diff --git a/packages/cellix/local-dev/src/vite.ts b/packages/cellix/local-dev/src/vite/index.ts similarity index 69% rename from packages/cellix/local-dev/src/vite.ts rename to packages/cellix/local-dev/src/vite/index.ts index 40c9b0b15..f50bd78b0 100644 --- a/packages/cellix/local-dev/src/vite.ts +++ b/packages/cellix/local-dev/src/vite/index.ts @@ -1,6 +1,9 @@ export interface BuildViteArgsOptions { + /** Host passed to Vite. Defaults to `127.0.0.1`. */ host?: string; + /** Optional port passed to Vite. */ port?: string; + /** Environment used to infer e2e mode. Defaults to `process.env`. */ env?: NodeJS.ProcessEnv; } @@ -12,6 +15,9 @@ type ViteEnv = NodeJS.ProcessEnv & { /** * Returns true when the current process is running in an e2e-oriented mode. + * + * @param env - Environment to inspect. Defaults to `process.env`. + * @returns Whether `E2E` is enabled with a truthy local-dev value. */ export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { const viteEnv = env as ViteEnv; @@ -20,6 +26,9 @@ export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { /** * Builds the shared argument list for Vite dev startup across Cellix apps. + * + * @param options - Host, port, and env values used to build Vite arguments. + * @returns CLI arguments suitable for `vite`. */ export function buildViteArgs(options: BuildViteArgsOptions = {}): string[] { const { host = '127.0.0.1', port, env = process.env } = options; diff --git a/packages/cellix/local-dev/src/workspace.ts b/packages/cellix/local-dev/src/workspace/index.ts similarity index 100% rename from packages/cellix/local-dev/src/workspace.ts rename to packages/cellix/local-dev/src/workspace/index.ts diff --git a/packages/cellix/local-dev/src/worktree/index.ts b/packages/cellix/local-dev/src/worktree/index.ts new file mode 100644 index 000000000..9a7fff5e7 --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/index.ts @@ -0,0 +1,10 @@ +export { WorktreeJsonFileSync, type WorktreeJsonFileSyncOptions } from './json-file-sync.ts'; +export { + type AzuritePorts, + buildAzuriteConnectionString, + getAzuritePorts, + getMongoPort, + getWorktreePortOffset, +} from './ports.ts'; +export { WorktreeSettings } from './settings.ts'; +export type { SettingsRecord, WorktreeMode, WorktreeSettingsOptions } from './types.ts'; diff --git a/packages/cellix/local-dev/src/worktree/json-file-sync.ts b/packages/cellix/local-dev/src/worktree/json-file-sync.ts new file mode 100644 index 000000000..105a01f18 --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/json-file-sync.ts @@ -0,0 +1,89 @@ +import { existsSync } from 'node:fs'; +import { syncJsonFile } from '../files/json.ts'; +import { buildAzuriteConnectionString, getAzuritePorts } from './ports.ts'; +import { WorktreeSettings } from './settings.ts'; +import type { KnownWorktreeSettings, SettingsDocument, SettingsRecord, WorktreeSettingsOptions } from './types.ts'; +import { resolveWorktreeName } from './worktree-name.ts'; + +export interface WorktreeJsonFileSyncOptions extends WorktreeSettingsOptions { + /** Source JSON document to copy or transform. */ + sourcePath: string; + /** Destination path. Parent directories are created automatically. */ + targetPath: string; + /** Skip syncing when the source file is missing. Defaults to `false`. */ + skipIfMissing?: boolean; + /** Extra settings merged into the JSON document's `Values` object. */ + values?: SettingsRecord; + /** Keys that should receive a worktree-scoped Azurite connection string. */ + azuriteConnectionStringKeys?: string[]; +} + +/** + * Syncs a JSON settings file while applying generic worktree transforms. + * + * The helper is intentionally schema-light: it preserves the source document, + * merges caller-provided values into `Values`, and only applies generic URL, + * MongoDB, port, and optional Azurite connection-string transforms. + */ +export class WorktreeJsonFileSync { + private readonly options: WorktreeJsonFileSyncOptions; + + public constructor(options: WorktreeJsonFileSyncOptions) { + this.options = options; + } + + /** + * Copies or transforms the configured JSON source into the target path. + * + * @throws When the source file is missing and `skipIfMissing` is not true. + */ + public sync(): void { + if (this.options.skipIfMissing && !existsSync(this.options.sourcePath)) { + return; + } + + syncJsonFile({ + sourcePath: this.options.sourcePath, + targetPath: this.options.targetPath, + transform: (document) => this.transformDocument(document), + }); + } + + private transformDocument(document: SettingsDocument): SettingsDocument { + const values = { + ...(document.Values ?? {}), + ...(this.options.values ?? {}), + }; + const env = this.options.env ?? process.env; + const worktreeName = resolveWorktreeName({ + env, + ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), + ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), + }); + const settings = new WorktreeSettings({ + env, + ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), + ...(worktreeName ? { worktreeName } : {}), + }); + const transformedValues = settings.transformRecord(values) as KnownWorktreeSettings; + const accountName = String(transformedValues.STORAGE_ACCOUNT_NAME ?? ''); + const accountKey = String(transformedValues.STORAGE_ACCOUNT_KEY ?? ''); + + if (accountName && accountKey && worktreeName) { + const connectionString = buildAzuriteConnectionString({ + accountName, + accountKey, + ports: getAzuritePorts(worktreeName), + }); + + for (const key of this.options.azuriteConnectionStringKeys ?? []) { + transformedValues[key] = connectionString; + } + } + + return { + ...document, + Values: transformedValues, + }; + } +} diff --git a/packages/cellix/local-dev/src/worktree-ports.ts b/packages/cellix/local-dev/src/worktree/ports.ts similarity index 100% rename from packages/cellix/local-dev/src/worktree-ports.ts rename to packages/cellix/local-dev/src/worktree/ports.ts diff --git a/packages/cellix/local-dev/src/worktree/settings.ts b/packages/cellix/local-dev/src/worktree/settings.ts new file mode 100644 index 000000000..3c677b5f3 --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/settings.ts @@ -0,0 +1,117 @@ +import { applyWorktreeSuffix, replaceUrlPort } from '../urls/index.ts'; +import { getMongoPort, getWorktreePortOffset } from './ports.ts'; +import type { KnownWorktreeSettings, SettingsRecord, WorktreeSettingsOptions } from './types.ts'; +import { resolveWorktreeName } from './worktree-name.ts'; + +function isPlainRecord(value: unknown): value is SettingsRecord { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function stringifyEnvValue(value: unknown): string | undefined { + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + return undefined; +} + +function transformUrl(urlValue: string, worktreeName: string | undefined): string { + try { + const url = new URL(urlValue); + if (url.protocol === 'mongodb:') { + return worktreeName ? replaceUrlPort(urlValue, getMongoPort(worktreeName)) : urlValue; + } + + if (url.hostname) { + url.hostname = applyWorktreeSuffix(url.hostname, worktreeName); + } + + const transformedUrl = url.toString(); + if (!urlValue.endsWith('/') && url.pathname === '/' && !url.search && !url.hash) { + return transformedUrl.slice(0, -1); + } + + return transformedUrl; + } catch { + return urlValue; + } +} + +function transformStringValue(value: string, worktreeName: string | undefined): string { + const withHttpUrls = value.replace(/https?:\/\/[^\s;'"<>]+/g, (match) => transformUrl(match, worktreeName)); + return withHttpUrls.replace(/mongodb:\/\/[^\s;'"<>]+/g, (match) => transformUrl(match, worktreeName)); +} + +function transformValue(value: unknown, worktreeName: string | undefined): unknown { + if (typeof value === 'string') { + return transformStringValue(value, worktreeName); + } + + if (Array.isArray(value)) { + return value.map((item) => transformValue(item, worktreeName)); + } + + if (isPlainRecord(value)) { + return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, transformValue(entryValue, worktreeName)])); + } + + return value; +} + +/** + * Applies deterministic worktree transforms to caller-provided local-dev + * settings without knowing the consuming application's env-key policy. + * + * URL-like strings receive `.localhost` hostname suffixes, MongoDB URLs receive + * worktree-specific ports, and `PORT` receives the generic worktree port offset. + */ +export class WorktreeSettings { + private readonly env: NodeJS.ProcessEnv; + private readonly settings: SettingsRecord; + private readonly worktreeName: string | undefined; + + public constructor(options: WorktreeSettingsOptions = {}) { + this.env = options.env ?? process.env; + this.settings = options.settings ?? {}; + this.worktreeName = resolveWorktreeName({ ...options, env: this.env }); + } + + /** + * Returns a process environment with transformed settings merged over the + * base env. + * + * Non-string, non-number, and non-boolean setting values are ignored because + * process environments can only carry strings. + * + * @returns A new environment object. + */ + public toEnv(): NodeJS.ProcessEnv { + const transformedSettings = this.transformRecord(this.settings); + return { + ...this.env, + ...Object.fromEntries( + Object.entries(transformedSettings) + .map(([key, value]) => [key, stringifyEnvValue(value)]) + .filter((entry): entry is [string, string] => typeof entry[1] === 'string'), + ), + }; + } + + /** + * Applies worktree transforms to a settings record while preserving its + * object shape. + * + * @param record - Settings object to transform. + * @returns A transformed copy of the provided record. + */ + public transformRecord(record: TRecord): TRecord { + const transformed = Object.fromEntries(Object.entries(record).map(([key, value]) => [key, transformValue(value, this.worktreeName)])) as KnownWorktreeSettings; + + if (typeof transformed.PORT === 'string' && this.worktreeName) { + const port = Number(transformed.PORT); + if (Number.isInteger(port)) { + transformed.PORT = String(port + getWorktreePortOffset(this.worktreeName)); + } + } + + return transformed as TRecord; + } +} diff --git a/packages/cellix/local-dev/src/worktree/types.ts b/packages/cellix/local-dev/src/worktree/types.ts new file mode 100644 index 000000000..a7694dcbd --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/types.ts @@ -0,0 +1,57 @@ +/** + * JSON-compatible settings object accepted by worktree transformers. + */ +export type SettingsRecord = Record; + +/** + * Settings keys with special generic worktree behavior. + */ +export type KnownWorktreeSettings = SettingsRecord & { + /** Numeric port string offset when a worktree is active. */ + PORT?: unknown; + /** Azurite storage key used to build connection strings. */ + STORAGE_ACCOUNT_KEY?: unknown; + /** Azurite storage account used to build connection strings. */ + STORAGE_ACCOUNT_NAME?: unknown; +}; + +/** + * Azure Functions-style settings JSON document. + */ +export interface SettingsDocument { + /** Runtime values merged and transformed by local-dev helpers. */ + Values?: SettingsRecord; + [key: string]: unknown; +} + +/** + * Environment shape used by worktree helpers. + */ +export type WorktreeEnv = NodeJS.ProcessEnv & { + /** Explicit toggle for worktree transforms. */ + CELLIX_WORKTREE?: string; + /** Raw worktree name used for hostname suffixes and port offsets. */ + WORKTREE_NAME?: string; +}; + +/** + * Explicit worktree transform mode. + * + * `true` enables transforms when a name is available, and `false` disables + * transforms even if `WORKTREE_NAME` is present in the environment. + */ +export type WorktreeMode = boolean; + +/** + * Options shared by worktree env and settings transformers. + */ +export interface WorktreeSettingsOptions { + /** Base environment. Defaults to `process.env`. */ + env?: NodeJS.ProcessEnv; + /** Settings to merge over `env` before worktree transforms are applied. */ + settings?: SettingsRecord; + /** Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, then auto mode. */ + worktree?: WorktreeMode; + /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ + worktreeName?: string; +} diff --git a/packages/cellix/local-dev/src/worktree/worktree-name.ts b/packages/cellix/local-dev/src/worktree/worktree-name.ts new file mode 100644 index 000000000..8cbfbbc45 --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/worktree-name.ts @@ -0,0 +1,31 @@ +import type { WorktreeEnv, WorktreeMode } from './types.ts'; + +function parseWorktreeMode(value: string | undefined): WorktreeMode | undefined { + if (!value) return undefined; + + const normalized = value.toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + + return undefined; +} + +/** + * Resolves the effective worktree name for a local-dev operation. + * + * `worktree: false` or `CELLIX_WORKTREE=0` disables worktree behavior even + * when `WORKTREE_NAME` is present. Invalid `CELLIX_WORKTREE` values are ignored + * so callers can fall back to automatic mode. + * + * @param options - Optional env, explicit mode, and explicit worktree name. + * @returns The worktree name to apply, or `undefined` when transforms should be + * disabled or no name is available. + */ +export function resolveWorktreeName(options: { env?: NodeJS.ProcessEnv; worktree?: WorktreeMode; worktreeName?: string }): string | undefined { + const env = (options.env ?? process.env) as WorktreeEnv; + const worktreeMode = options.worktree ?? parseWorktreeMode(env.CELLIX_WORKTREE); + + if (worktreeMode === false) return undefined; + + return options.worktreeName ?? env.WORKTREE_NAME; +} diff --git a/packages/ocom/local-dev-config/README.md b/packages/ocom/local-dev-config/README.md index 9e22cc015..fa6a18c24 100644 --- a/packages/ocom/local-dev-config/README.md +++ b/packages/ocom/local-dev-config/README.md @@ -16,28 +16,32 @@ This package owns Owner Community application policy: which app `.env` files def ## Usage -Use this package from app-owned `start-dev.ts` or settings-sync wrappers: +Use this package as the OCOM source of local URLs, then pass those values into the generic Cellix worktree runners: ```ts -import { runViteDev } from '@cellix/local-dev'; +import { WorktreeViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; const urls = buildOcomUrls(); -runViteDev({ - env: { - ...process.env, - VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, +new WorktreeViteDevRunner({ + settings: { VITE_APP_UI_COMMUNITY_BASE_URL: urls.uiCommunityBaseUrl, + VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, }, -}); +}).start(); ``` ## Public API +All exports are available from `@ocom/local-dev-config`. Focused subpaths are +also published for narrower imports: + +- `@ocom/local-dev-config/hostnames` +- `@ocom/local-dev-config/urls` + - `getOcomHostnames(options?)` - `buildOcomUrls(options?)` -- `getWorkspaceRoot(startDir?)` - `OcomLocalDevOptions` - `OcomHostnames` - `OcomUrls` @@ -47,3 +51,4 @@ runViteDev({ - Keep reusable process runners, dotenv parsing, JSON syncing, and port math in `@cellix/local-dev`. - Keep OCOM-specific hostname derivation, auth paths, redirect paths, and app `.env` lookup policy here. - Keep one-off runtime behavior in the consuming app wrapper script instead of widening this package. +- Keep app wrapper scripts thin: get OCOM URL values here, pass them into a generic `@cellix/local-dev` worktree object, and call `start()` or `sync()`. diff --git a/packages/ocom/local-dev-config/manifest.md b/packages/ocom/local-dev-config/manifest.md index 533fcd55b..583917937 100644 --- a/packages/ocom/local-dev-config/manifest.md +++ b/packages/ocom/local-dev-config/manifest.md @@ -6,29 +6,36 @@ Provide Owner Community local-development URL and hostname policy as a reusable ## Scope -This package resolves OCOM hostnames from app `.env` files and environment overrides, applies worktree-safe host suffixing through `@cellix/local-dev`, and builds the complete local URL set needed by UI, API, mock-auth, and docs wrapper scripts. +This package resolves OCOM hostnames from app `.env` files and environment overrides, applies the shared hostname suffixing helper from `@cellix/local-dev`, and builds the complete local URL set needed by UI, API, mock-auth, docs, e2e, and other build-time consumers. ## Non-goals - Generic process runners, port math, JSON syncing, or dotenv parsing - Production runtime configuration - Non-OCOM app defaults -- Starting local dev processes directly +- Generic local dev process orchestration ## Public API shape +Published entrypoints: + +- `@ocom/local-dev-config` +- `@ocom/local-dev-config/hostnames` +- `@ocom/local-dev-config/urls` + +Root entrypoint exports: + - `getOcomHostnames(options?)` - `buildOcomUrls(options?)` -- `getWorkspaceRoot(startDir?)` - `OcomLocalDevOptions` - `OcomHostnames` - `OcomUrls` ## Core concepts -- OCOM app wrappers should compose this package with `@cellix/local-dev`. +- OCOM app wrappers should use this package to get OCOM URL values, then pass those values into generic `@cellix/local-dev` worktree-aware runners or settings syncers. - Environment values override app `.env` file values so task runners can inject per-process configuration. -- Worktree suffixing is delegated to `@cellix/local-dev` so all participating apps share one hostname-safety and idempotency rule. +- Worktree suffixing, Mongo ports, Azurite ports, and settings-file connection-string transforms are delegated to `@cellix/local-dev` so all participating apps share one rule. ## Package boundaries diff --git a/packages/ocom/local-dev-config/package.json b/packages/ocom/local-dev-config/package.json index 33937e308..ba177169d 100644 --- a/packages/ocom/local-dev-config/package.json +++ b/packages/ocom/local-dev-config/package.json @@ -15,16 +15,12 @@ "default": "./dist/index.js" }, "./hostnames": { - "types": "./dist/hostnames.d.ts", - "default": "./dist/hostnames.js" + "types": "./dist/hostnames/index.d.ts", + "default": "./dist/hostnames/index.js" }, "./urls": { - "types": "./dist/urls.d.ts", - "default": "./dist/urls.js" - }, - "./workspace": { - "types": "./dist/workspace.d.ts", - "default": "./dist/workspace.js" + "types": "./dist/urls/index.d.ts", + "default": "./dist/urls/index.js" } }, "scripts": { diff --git a/packages/ocom/local-dev-config/src/api-settings/index.ts b/packages/ocom/local-dev-config/src/api-settings/index.ts new file mode 100644 index 000000000..a940549ed --- /dev/null +++ b/packages/ocom/local-dev-config/src/api-settings/index.ts @@ -0,0 +1,31 @@ +import type { AzureFunctionsLocalSettingsOptions } from '@cellix/local-dev'; +import type { OcomLocalDevOptions } from '../types.ts'; +import { buildOcomUrls } from '../urls/index.ts'; + +/** + * Builds the OCOM api's Azure Functions local-settings policy. + * + * This is the single source of truth for the api's `local.settings.json` + * preparation, consumed by both the dev runner (`start-dev.ts`) and the + * standalone sync (`sync-local-settings.ts`). It maps the mock OIDC issuer and + * JWKS URLs into the E2E settings and names the keys that should receive a + * worktree-scoped Azurite connection string. + * + * @param options - Optional environment and workspace-root overrides forwarded + * to `buildOcomUrls`. + * @returns Local-settings options for `AzureFunctionsDevRunner` / + * `AzureFunctionsLocalSettings`. + */ +export function buildOcomApiLocalSettings(options: OcomLocalDevOptions = {}): AzureFunctionsLocalSettingsOptions { + const urls = buildOcomUrls(options); + + return { + e2eValues: { + ACCOUNT_PORTAL_OIDC_ISSUER: urls.mockCommunityAuthorityUrl, + ACCOUNT_PORTAL_OIDC_ENDPOINT: urls.mockCommunityJwksUrl, + STAFF_PORTAL_OIDC_ISSUER: urls.mockStaffAuthorityUrl, + STAFF_PORTAL_OIDC_ENDPOINT: urls.mockStaffJwksUrl, + }, + azuriteConnectionStringKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], + }; +} diff --git a/packages/ocom/local-dev-config/src/hostnames.ts b/packages/ocom/local-dev-config/src/hostnames/index.ts similarity index 86% rename from packages/ocom/local-dev-config/src/hostnames.ts rename to packages/ocom/local-dev-config/src/hostnames/index.ts index c39aa56ef..f55981f2a 100644 --- a/packages/ocom/local-dev-config/src/hostnames.ts +++ b/packages/ocom/local-dev-config/src/hostnames/index.ts @@ -1,7 +1,8 @@ import path from 'node:path'; -import { applyWorktreeSuffix, type DotEnvValues, hostnameFromUrl, readDotEnv } from '@cellix/local-dev'; -import type { OcomHostnames, OcomLocalDevOptions } from './types.ts'; -import { getWorkspaceRoot } from './workspace.ts'; +import { type DotEnvValues, readDotEnv } from '@cellix/local-dev/files'; +import { applyWorktreeSuffix, hostnameFromUrl } from '@cellix/local-dev/urls'; +import { resolveWorkspaceRoot } from '@cellix/local-dev/workspace'; +import type { OcomHostnames, OcomLocalDevOptions } from '../types.ts'; interface OcomEnvValues { WORKTREE_NAME?: string; @@ -48,7 +49,7 @@ function readAppEnv(workspaceRoot: string, appName: string): DotEnvValues & Ocom */ export function getOcomHostnames(options: OcomLocalDevOptions = {}): OcomHostnames { const env = (options.env ?? process.env) as NodeJS.ProcessEnv & OcomEnvValues; - const workspaceRoot = options.workspaceRoot ?? getWorkspaceRoot(); + const workspaceRoot = options.workspaceRoot ?? resolveWorkspaceRoot(); const communityEnv = readAppEnv(workspaceRoot, 'ui-community'); const staffEnv = readAppEnv(workspaceRoot, 'ui-staff'); const worktreeName = env.WORKTREE_NAME; @@ -69,4 +70,4 @@ export function getOcomHostnames(options: OcomLocalDevOptions = {}): OcomHostnam }; } -export type { OcomHostnames, OcomLocalDevOptions } from './types.ts'; +export type { OcomHostnames, OcomLocalDevOptions } from '../types.ts'; diff --git a/packages/ocom/local-dev-config/src/index.test.ts b/packages/ocom/local-dev-config/src/index.test.ts index 090fdd712..200bdb10c 100644 --- a/packages/ocom/local-dev-config/src/index.test.ts +++ b/packages/ocom/local-dev-config/src/index.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { buildOcomUrls, getOcomHostnames, getWorkspaceRoot } from '@ocom/local-dev-config'; +import { buildOcomUrls, getOcomHostnames } from '@ocom/local-dev-config'; import { describe, expect, it } from 'vitest'; function createWorkspaceFixture(): string { @@ -57,13 +57,6 @@ describe('@ocom/local-dev-config', () => { }); }); - it('resolves the workspace root from a nested OCOM package directory', () => { - const workspaceRoot = createWorkspaceFixture(); - const nestedDir = path.join(workspaceRoot, 'apps', 'ui-community'); - - expect(getWorkspaceRoot(nestedDir)).toBe(workspaceRoot); - }); - it('lets environment values override app env files', () => { const workspaceRoot = createWorkspaceFixture(); diff --git a/packages/ocom/local-dev-config/src/index.ts b/packages/ocom/local-dev-config/src/index.ts index 93cbebc32..c265511f7 100644 --- a/packages/ocom/local-dev-config/src/index.ts +++ b/packages/ocom/local-dev-config/src/index.ts @@ -1,4 +1,4 @@ -export { getOcomHostnames } from './hostnames.ts'; +export { buildOcomApiLocalSettings } from './api-settings/index.ts'; +export { getOcomHostnames } from './hostnames/index.ts'; export type { OcomHostnames, OcomLocalDevOptions, OcomUrls } from './types.ts'; -export { buildOcomUrls } from './urls.ts'; -export { getWorkspaceRoot } from './workspace.ts'; +export { buildOcomUrls } from './urls/index.ts'; diff --git a/packages/ocom/local-dev-config/src/urls.ts b/packages/ocom/local-dev-config/src/urls/index.ts similarity index 83% rename from packages/ocom/local-dev-config/src/urls.ts rename to packages/ocom/local-dev-config/src/urls/index.ts index 88231ae2d..c05d6cf9e 100644 --- a/packages/ocom/local-dev-config/src/urls.ts +++ b/packages/ocom/local-dev-config/src/urls/index.ts @@ -1,6 +1,6 @@ -import { buildPortlessUrl } from '@cellix/local-dev'; -import { getOcomHostnames } from './hostnames.ts'; -import type { OcomLocalDevOptions, OcomUrls } from './types.ts'; +import { buildPortlessUrl } from '@cellix/local-dev/urls'; +import { getOcomHostnames } from '../hostnames/index.ts'; +import type { OcomLocalDevOptions, OcomUrls } from '../types.ts'; /** * Builds the full set of local URLs required by OCOM app wrapper scripts. @@ -33,4 +33,4 @@ export function buildOcomUrls(options: OcomLocalDevOptions = {}): OcomUrls { }; } -export type { OcomLocalDevOptions, OcomUrls } from './types.ts'; +export type { OcomLocalDevOptions, OcomUrls } from '../types.ts'; diff --git a/packages/ocom/local-dev-config/src/workspace.ts b/packages/ocom/local-dev-config/src/workspace.ts deleted file mode 100644 index a8db0065a..000000000 --- a/packages/ocom/local-dev-config/src/workspace.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { resolveWorkspaceRoot } from '@cellix/local-dev'; - -/** - * Resolves the OCOM workspace root using the shared Cellix workspace lookup. - * - * @param startDir - Directory to start searching from. Defaults to the current - * working directory. - * @returns Absolute workspace root path. - */ -export function getWorkspaceRoot(startDir: string = process.cwd()): string { - return resolveWorkspaceRoot({ startDir }); -} diff --git a/packages/ocom/local-dev-config/vitest.config.ts b/packages/ocom/local-dev-config/vitest.config.ts index 422905798..2338cbf45 100644 --- a/packages/ocom/local-dev-config/vitest.config.ts +++ b/packages/ocom/local-dev-config/vitest.config.ts @@ -11,9 +11,18 @@ export default mergeConfig(nodeConfig, { }, }, resolve: { - alias: { - '@ocom/local-dev-config': join(dirname, 'src/index.ts'), - '@cellix/local-dev': join(dirname, '../../cellix/local-dev/src/index.ts'), - }, + alias: [ + { find: '@ocom/local-dev-config/hostnames', replacement: join(dirname, 'src/hostnames/index.ts') }, + { find: '@ocom/local-dev-config/urls', replacement: join(dirname, 'src/urls/index.ts') }, + { find: '@ocom/local-dev-config', replacement: join(dirname, 'src/index.ts') }, + { find: '@cellix/local-dev/files', replacement: join(dirname, '../../cellix/local-dev/src/files/index.ts') }, + { find: '@cellix/local-dev/process', replacement: join(dirname, '../../cellix/local-dev/src/process/index.ts') }, + { find: '@cellix/local-dev/runners', replacement: join(dirname, '../../cellix/local-dev/src/runners/index.ts') }, + { find: '@cellix/local-dev/urls', replacement: join(dirname, '../../cellix/local-dev/src/urls/index.ts') }, + { find: '@cellix/local-dev/vite', replacement: join(dirname, '../../cellix/local-dev/src/vite/index.ts') }, + { find: '@cellix/local-dev/workspace', replacement: join(dirname, '../../cellix/local-dev/src/workspace/index.ts') }, + { find: '@cellix/local-dev/worktree', replacement: join(dirname, '../../cellix/local-dev/src/worktree/index.ts') }, + { find: '@cellix/local-dev', replacement: join(dirname, '../../cellix/local-dev/src/index.ts') }, + ], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index caedd6eeb..1e16c4f37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,9 +105,6 @@ catalogs: storybook: specifier: 10.4.2 version: 10.4.2 - tsx: - specifier: ^4.21.0 - version: 4.21.0 typescript: specifier: 6.0.3 version: 6.0.3 @@ -119,6 +116,7 @@ catalogs: version: 4.1.6 overrides: + '@grpc/grpc-js': ^1.14.4 '@vitest/browser': 4.1.6 '@vitest/browser-playwright': 4.1.6 axios: 1.16.0 @@ -473,9 +471,6 @@ importers: rimraf: specifier: 'catalog:' version: 6.0.1 - tsx: - specifier: 'catalog:' - version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.3 @@ -4817,8 +4812,8 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} engines: {node: '>=12.10.0'} '@grpc/proto-loader@0.8.0': @@ -17144,7 +17139,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@grpc/grpc-js@1.14.3': + '@grpc/grpc-js@1.14.4': dependencies: '@grpc/proto-loader': 0.8.0 '@js-sdsl/ordered-map': 4.4.2 @@ -17402,7 +17397,7 @@ snapshots: '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) @@ -17432,7 +17427,7 @@ snapshots: '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) @@ -17471,7 +17466,7 @@ snapshots: '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) @@ -17591,7 +17586,7 @@ snapshots: '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f6f6f9633..ffb5f1b04 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -67,6 +67,7 @@ allowBuilds: snyk: true overrides: + '@grpc/grpc-js': ^1.14.4 '@vitest/browser': 4.1.6 '@vitest/browser-playwright': 4.1.6 axios: 1.16.0 From 8d1edc2f0d99448bddfb3827098acaf002d73df5 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 11 Jun 2026 14:23:55 -0400 Subject: [PATCH 26/40] small adjustents to pattern for easier readability --- apps/ui-community/start-dev.ts | 1 - apps/ui-staff/start-dev.ts | 1 - .../runners/azure-functions-local-settings.ts | 28 +++++++--- .../local-dev/src/runners/azure-functions.ts | 2 +- .../local-dev/src/worktree/json-file-sync.ts | 56 ++++++++++--------- 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/apps/ui-community/start-dev.ts b/apps/ui-community/start-dev.ts index 3017f8859..2be927929 100644 --- a/apps/ui-community/start-dev.ts +++ b/apps/ui-community/start-dev.ts @@ -1,4 +1,3 @@ -/// import { ViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; diff --git a/apps/ui-staff/start-dev.ts b/apps/ui-staff/start-dev.ts index 0257cd6b0..5abd14596 100644 --- a/apps/ui-staff/start-dev.ts +++ b/apps/ui-staff/start-dev.ts @@ -1,4 +1,3 @@ -/// import { ViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts index 6752ed3cd..33eb16be6 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -61,17 +61,31 @@ export class AzureFunctionsLocalSettings { const { appDir = process.cwd(), scriptRoot = 'deploy/', env = process.env } = this.options; const e2e = isE2E(env); + // Which settings file we copy from: E2E uses the mock-OIDC settings, + // normal dev uses the checked-in local.settings.json. + const sourcePath = e2e ? (this.options.e2eSourcePath ?? path.join(appDir, 'local-settings.e2e.json')) : (this.options.sourcePath ?? path.join(appDir, 'local.settings.json')); + + // Where Azure Functions expects it: local.settings.json under the script root. + const targetPath = this.options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'); + + // E2E must fail loudly if its source is missing; normal dev tolerates a + // missing local.settings.json (the sync becomes a no-op). + const skipIfMissing = !e2e; + + // Base values always apply; E2E-only overrides are layered on top in E2E mode. + const values: SettingsRecord = { ...this.options.values }; + if (e2e && this.options.e2eValues) { + Object.assign(values, this.options.e2eValues); + } + new WorktreeJsonFileSync({ env, ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), - sourcePath: e2e ? (this.options.e2eSourcePath ?? path.join(appDir, 'local-settings.e2e.json')) : (this.options.sourcePath ?? path.join(appDir, 'local.settings.json')), - targetPath: this.options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'), - skipIfMissing: !e2e, - values: { - ...(this.options.values ?? {}), - ...(e2e ? (this.options.e2eValues ?? {}) : {}), - }, + sourcePath, + targetPath, + skipIfMissing, + values, ...(this.options.azuriteConnectionStringKeys ? { azuriteConnectionStringKeys: this.options.azuriteConnectionStringKeys } : {}), }).sync(); } diff --git a/packages/cellix/local-dev/src/runners/azure-functions.ts b/packages/cellix/local-dev/src/runners/azure-functions.ts index 246c7e651..1ed065557 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions.ts @@ -74,7 +74,7 @@ export class AzureFunctionsDevRunner { const worktreeContext = resolveWorktreeContext(this.options); const env = new WorktreeSettings({ ...this.options, ...worktreeContext }).toEnv() as RunnerEnv; - const localSettings = this.options.localSettings; + const { localSettings } = this.options; if (localSettings) { const scriptRoot = localSettings.scriptRoot ?? this.options.scriptRoot; new AzureFunctionsLocalSettings({ diff --git a/packages/cellix/local-dev/src/worktree/json-file-sync.ts b/packages/cellix/local-dev/src/worktree/json-file-sync.ts index 105a01f18..a759bc61a 100644 --- a/packages/cellix/local-dev/src/worktree/json-file-sync.ts +++ b/packages/cellix/local-dev/src/worktree/json-file-sync.ts @@ -50,35 +50,17 @@ export class WorktreeJsonFileSync { } private transformDocument(document: SettingsDocument): SettingsDocument { - const values = { + const env = this.options.env ?? process.env; + const worktreeName = resolveWorktreeName({ ...this.options, env }); + + const values: SettingsRecord = { ...(document.Values ?? {}), ...(this.options.values ?? {}), }; - const env = this.options.env ?? process.env; - const worktreeName = resolveWorktreeName({ - env, - ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), - ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), - }); - const settings = new WorktreeSettings({ - env, - ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), - ...(worktreeName ? { worktreeName } : {}), - }); - const transformedValues = settings.transformRecord(values) as KnownWorktreeSettings; - const accountName = String(transformedValues.STORAGE_ACCOUNT_NAME ?? ''); - const accountKey = String(transformedValues.STORAGE_ACCOUNT_KEY ?? ''); - - if (accountName && accountKey && worktreeName) { - const connectionString = buildAzuriteConnectionString({ - accountName, - accountKey, - ports: getAzuritePorts(worktreeName), - }); + const transformedValues = new WorktreeSettings(worktreeName ? { env, worktreeName } : { env, worktree: false }).transformRecord(values) as KnownWorktreeSettings; - for (const key of this.options.azuriteConnectionStringKeys ?? []) { - transformedValues[key] = connectionString; - } + if (worktreeName) { + this.applyAzuriteConnectionString(transformedValues, worktreeName); } return { @@ -86,4 +68,28 @@ export class WorktreeJsonFileSync { Values: transformedValues, }; } + + /** + * Replaces each configured key with a worktree-scoped Azurite connection + * string, when the document carries Azurite account credentials. + * + * @param values - Transformed settings to mutate in place. + * @param worktreeName - Active worktree used to derive Azurite ports. + */ + private applyAzuriteConnectionString(values: KnownWorktreeSettings, worktreeName: string): void { + const accountName = String(values.STORAGE_ACCOUNT_NAME ?? ''); + const accountKey = String(values.STORAGE_ACCOUNT_KEY ?? ''); + if (!accountName || !accountKey) { + return; + } + + const connectionString = buildAzuriteConnectionString({ + accountName, + accountKey, + ports: getAzuritePorts(worktreeName), + }); + for (const key of this.options.azuriteConnectionStringKeys ?? []) { + values[key] = connectionString; + } + } } From 31cda3b64c6c3b81c8ba51360d72d4f8e5e29fd6 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 11 Jun 2026 16:02:30 -0400 Subject: [PATCH 27/40] made clearer path for how worktree conversion was done - logic was a bit buried in code --- packages/cellix/local-dev/src/index.test.ts | 140 +++++++++--------- packages/cellix/local-dev/src/index.ts | 4 +- .../runners/azure-functions-local-settings.ts | 75 ++++------ .../local-dev/src/worktree/conversion.ts | 85 +++++++++++ .../cellix/local-dev/src/worktree/index.ts | 2 +- .../local-dev/src/worktree/json-file-sync.ts | 95 ------------ .../cellix/local-dev/src/worktree/types.ts | 9 -- .../src/api-settings/index.ts | 66 ++++++--- 8 files changed, 235 insertions(+), 241 deletions(-) create mode 100644 packages/cellix/local-dev/src/worktree/conversion.ts delete mode 100644 packages/cellix/local-dev/src/worktree/json-file-sync.ts diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 499bb9ee0..4c02bad11 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -9,6 +9,7 @@ import { buildAzuriteConnectionString, buildPortlessUrl, buildViteArgs, + convertSettingsForWorktree, getAzuritePorts, getMongoPort, getWorktreePortOffset, @@ -22,7 +23,6 @@ import { sanitizeWorktreeHostnameLabel, syncJsonFile, ViteDevRunner, - WorktreeJsonFileSync, WorktreeSettings, } from '@cellix/local-dev'; import { describe, expect, it } from 'vitest'; @@ -264,74 +264,37 @@ describe('@cellix/local-dev', () => { }); }); - it('syncs json settings with generic worktree URL, Mongo, and Azurite transforms', () => { - const workspaceRoot = createWorkspaceFixture(); - const sourcePath = path.join(workspaceRoot, 'fixtures', 'settings.e2e.json'); - const targetPath = path.join(workspaceRoot, 'fixtures', 'deploy', 'local.settings.json'); - writeFileSync( - sourcePath, - JSON.stringify({ - Values: { - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - }, - }), - ); - - new WorktreeJsonFileSync({ - env: { WORKTREE_NAME: 'Jason/Feature 123' }, - sourcePath, - targetPath, - values: { + it('converts only the named settings for a worktree, leaving the rest untouched', () => { + const converted = convertSettingsForWorktree( + { ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + COSMOSDB_DBNAME: 'ocom', + }, + 'Jason/Feature 123', + { + urlKeys: ['ACCOUNT_PORTAL_OIDC_ISSUER'], + mongoKeys: ['COSMOSDB_CONNECTION_STRING'], + azuriteKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], }, - azuriteConnectionStringKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], - }).sync(); + ); - const settings = JSON.parse(readFileSync(targetPath, 'utf8')); - expect(settings.Values).toMatchObject({ + expect(converted).toMatchObject({ ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', AZURE_STORAGE_CONNECTION_STRING: expect.stringContaining('BlobEndpoint=http://127.0.0.1:'), AzureWebJobsStorage: expect.stringContaining('QueueEndpoint=http://127.0.0.1:'), + // Unlisted keys are passed through untouched. + COSMOSDB_DBNAME: 'ocom', + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', }); - expect(settings.Values.COSMOSDB_CONNECTION_STRING).not.toBe('mongodb://127.0.0.1:50000/ocom'); + expect(converted['COSMOSDB_CONNECTION_STRING']).toBe(`mongodb://127.0.0.1:${getMongoPort('Jason/Feature 123')}/ocom`); }); - it('syncs json settings without worktree transforms when CELLIX_WORKTREE disables them', () => { - const workspaceRoot = createWorkspaceFixture(); - const sourcePath = path.join(workspaceRoot, 'fixtures', 'settings.local.json'); - const targetPath = path.join(workspaceRoot, 'fixtures', 'deploy', 'local.settings.json'); - writeFileSync( - sourcePath, - JSON.stringify({ - Values: { - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - }, - }), - ); - - new WorktreeJsonFileSync({ - env: { CELLIX_WORKTREE: '0', WORKTREE_NAME: 'Jason/Feature 123' }, - sourcePath, - targetPath, - values: { - ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', - }, - azuriteConnectionStringKeys: ['AzureWebJobsStorage'], - }).sync(); - - const settings = JSON.parse(readFileSync(targetPath, 'utf8')); - expect(settings.Values).toMatchObject({ - ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - }); - expect(settings.Values.AzureWebJobsStorage).toBeUndefined(); - }); - - it('prepares Azure Functions local settings before starting func', () => { + it('generates Azure Functions local settings before starting func', () => { const workspaceRoot = createWorkspaceFixture(); const appDir = path.join(workspaceRoot, 'fixtures', 'api'); const calls: Array<{ command: string; args: string[]; env?: NodeJS.ProcessEnv }> = []; @@ -343,26 +306,27 @@ describe('@cellix/local-dev', () => { }); return new EventEmitter() as ReturnType; }; - mkdirSync(appDir, { recursive: true }); - writeFileSync( - path.join(appDir, 'local-settings.e2e.json'), - JSON.stringify({ - Values: { - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - }, - }), - ); new AzureFunctionsDevRunner({ env: { E2E: 'true', PORT: '7071', WORKTREE_NAME: 'Jason/Feature 123' }, localSettings: { appDir, - e2eValues: { + values: { + STORAGE_ACCOUNT_NAME: 'devstoreaccount1', + STORAGE_ACCOUNT_KEY: 'key', + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + AzureWebJobsStorage: 'UseDevelopmentStorage=true', ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', + languageWorkers__node__arguments: '--inspect=5858', + }, + e2eValues: { + languageWorkers__node__arguments: '', + }, + worktreeConversion: { + urlKeys: ['ACCOUNT_PORTAL_OIDC_ISSUER'], + mongoKeys: ['COSMOSDB_CONNECTION_STRING'], + azuriteKeys: ['AzureWebJobsStorage'], }, - azuriteConnectionStringKeys: ['AzureWebJobsStorage'], }, spawn, }).start(); @@ -371,6 +335,8 @@ describe('@cellix/local-dev', () => { expect(settings.Values).toMatchObject({ ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', AzureWebJobsStorage: expect.stringContaining('BlobEndpoint=http://127.0.0.1:'), + // E2E override applied over the base value. + languageWorkers__node__arguments: '', }); expect(settings.Values.COSMOSDB_CONNECTION_STRING).not.toBe('mongodb://127.0.0.1:50000/ocom'); expect(calls[0]).toMatchObject({ @@ -378,4 +344,32 @@ describe('@cellix/local-dev', () => { args: ['start', '--typescript', '--script-root', 'deploy/', '--port', '7071', '--cors', '*'], }); }); + + it('skips worktree conversion outside a worktree', () => { + const workspaceRoot = createWorkspaceFixture(); + const appDir = path.join(workspaceRoot, 'fixtures', 'api'); + const spawn: RunnerSpawn = () => new EventEmitter() as ReturnType; + + new AzureFunctionsDevRunner({ + env: { PORT: '7071' }, + localSettings: { + appDir, + values: { + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + }, + worktreeConversion: { + mongoKeys: ['COSMOSDB_CONNECTION_STRING'], + azuriteKeys: ['AzureWebJobsStorage'], + }, + }, + spawn, + }).start(); + + const settings = JSON.parse(readFileSync(path.join(appDir, 'deploy', 'local.settings.json'), 'utf8')); + expect(settings.Values).toMatchObject({ + COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + }); + }); }); diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index 46e783511..c038ec91e 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -44,8 +44,8 @@ export { export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite/index.ts'; export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace/index.ts'; export { - WorktreeJsonFileSync, - type WorktreeJsonFileSyncOptions, + convertSettingsForWorktree, + type WorktreeConversionPlan, type WorktreeMode, WorktreeSettings, type WorktreeSettingsOptions, diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts index 33eb16be6..161236fd8 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -1,6 +1,8 @@ import path from 'node:path'; -import { WorktreeJsonFileSync } from '../worktree/json-file-sync.ts'; +import { writeJsonFile } from '../files/json.ts'; +import { convertSettingsForWorktree, type WorktreeConversionPlan } from '../worktree/conversion.ts'; import type { SettingsRecord, WorktreeMode } from '../worktree/types.ts'; +import { resolveWorktreeName } from '../worktree/worktree-name.ts'; type LocalSettingsEnv = NodeJS.ProcessEnv & { E2E?: string; @@ -11,7 +13,7 @@ function isE2E(env: NodeJS.ProcessEnv): boolean { } export interface AzureFunctionsLocalSettingsOptions { - /** App directory containing the source local settings files. Defaults to `process.cwd()`. */ + /** App directory used to resolve the default target path. Defaults to `process.cwd()`. */ appDir?: string; /** Functions script root. Defaults to `deploy/`. */ scriptRoot?: string; @@ -21,28 +23,24 @@ export interface AzureFunctionsLocalSettingsOptions { worktree?: WorktreeMode; /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ worktreeName?: string; - /** Source settings file for normal dev mode. Defaults to `/local.settings.json`. */ - sourcePath?: string; - /** Source settings file for E2E mode. Defaults to `/local-settings.e2e.json`. */ - e2eSourcePath?: string; /** Target settings file. Defaults to `//local.settings.json`. */ targetPath?: string; - /** Values merged into every mode before worktree transforms. */ + /** Functions `Values`, written in every mode. */ values?: SettingsRecord; - /** Values merged only when `E2E` is truthy. */ + /** `Values` overrides merged only when `E2E` is truthy. */ e2eValues?: SettingsRecord; - /** Keys that should receive a worktree-scoped Azurite connection string. */ - azuriteConnectionStringKeys?: string[]; + /** Functions `Host` block (e.g. `{ LocalHttpPort, CORS }`). */ + host?: SettingsRecord; + /** Which `Values` keys get worktree-scoped, and how. Applied only in a worktree. */ + worktreeConversion?: WorktreeConversionPlan; } /** - * Prepares Azure Functions `local.settings.json` before `func start`. + * Generates Azure Functions `local.settings.json` before `func start`. * - * Azure Functions reads `local.settings.json` from the script root. This helper - * copies the mode-appropriate source file into that location and applies the - * generic worktree URL, Mongo, and Azurite transforms. Normal dev reads - * `local.settings.json` when present; E2E mode reads `local-settings.e2e.json` - * and fails if that source is missing. + * The flow is a flat three steps: load the mode's settings, convert the + * caller-named keys for the active worktree (a no-op outside a worktree), and + * write the document the Functions host reads from the script root. */ export class AzureFunctionsLocalSettings { private readonly options: AzureFunctionsLocalSettingsOptions; @@ -52,41 +50,32 @@ export class AzureFunctionsLocalSettings { } /** - * Syncs the mode-appropriate local settings file into the Functions script - * root. - * - * @throws When E2E mode is active and its source file is missing. + * Writes the worktree-scoped settings into the Functions script root. */ public sync(): void { const { appDir = process.cwd(), scriptRoot = 'deploy/', env = process.env } = this.options; - const e2e = isE2E(env); - - // Which settings file we copy from: E2E uses the mock-OIDC settings, - // normal dev uses the checked-in local.settings.json. - const sourcePath = e2e ? (this.options.e2eSourcePath ?? path.join(appDir, 'local-settings.e2e.json')) : (this.options.sourcePath ?? path.join(appDir, 'local.settings.json')); - - // Where Azure Functions expects it: local.settings.json under the script root. const targetPath = this.options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'); + const worktreeName = resolveWorktreeName({ + env, + ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), + ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), + }); - // E2E must fail loudly if its source is missing; normal dev tolerates a - // missing local.settings.json (the sync becomes a no-op). - const skipIfMissing = !e2e; - - // Base values always apply; E2E-only overrides are layered on top in E2E mode. + // 1. LOAD — the settings for this mode (base, plus E2E overrides in E2E). const values: SettingsRecord = { ...this.options.values }; - if (e2e && this.options.e2eValues) { + if (isE2E(env) && this.options.e2eValues) { Object.assign(values, this.options.e2eValues); } - new WorktreeJsonFileSync({ - env, - ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), - ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), - sourcePath, - targetPath, - skipIfMissing, - values, - ...(this.options.azuriteConnectionStringKeys ? { azuriteConnectionStringKeys: this.options.azuriteConnectionStringKeys } : {}), - }).sync(); + // 2. CONVERT — scope the caller-named keys to the active worktree (skipped otherwise). + const converted = worktreeName && this.options.worktreeConversion ? convertSettingsForWorktree(values, worktreeName, this.options.worktreeConversion) : values; + + // 3. WRITE — emit local.settings.json for the Functions host. + writeJsonFile(targetPath, { + IsEncrypted: false, + Values: converted, + ConnectionStrings: {}, + ...(this.options.host ? { Host: this.options.host } : {}), + }); } } diff --git a/packages/cellix/local-dev/src/worktree/conversion.ts b/packages/cellix/local-dev/src/worktree/conversion.ts new file mode 100644 index 000000000..7a2db1c9c --- /dev/null +++ b/packages/cellix/local-dev/src/worktree/conversion.ts @@ -0,0 +1,85 @@ +import { applyWorktreeSuffix, replaceUrlPort } from '../urls/index.ts'; +import { buildAzuriteConnectionString, getAzuritePorts, getMongoPort } from './ports.ts'; +import type { SettingsRecord } from './types.ts'; + +/** + * Explicit description of which settings change for a worktree, and how. + * + * The caller names the keys; this package only knows the mechanics. Keys not + * listed here are passed through untouched. + */ +export interface WorktreeConversionPlan { + /** Keys whose `http(s)` URL value gets the worktree hostname suffix. */ + urlKeys?: string[]; + /** Keys whose `mongodb://` URL value gets the worktree Mongo port. */ + mongoKeys?: string[]; + /** + * Keys that receive a worktree-scoped Azurite connection string, built from + * the document's `STORAGE_ACCOUNT_NAME` / `STORAGE_ACCOUNT_KEY`. + */ + azuriteKeys?: string[]; +} + +/** + * Suffixes a URL's hostname with the worktree label, preserving the rest of the + * URL (path, query, and absence of a trailing slash). + */ +function suffixUrlHostname(value: string, worktreeName: string): string { + try { + const url = new URL(value); + url.hostname = applyWorktreeSuffix(url.hostname, worktreeName); + const result = url.toString(); + // `URL.toString()` appends a trailing slash to bare-origin URLs; keep input shape. + if (!value.endsWith('/') && url.pathname === '/' && !url.search && !url.hash) { + return result.slice(0, -1); + } + return result; + } catch { + return value; + } +} + +/** + * Returns a copy of `values` with the worktree conversion applied to exactly the + * keys named in `plan` — URL hostnames suffixed, Mongo ports shifted, and + * Azurite storage keys replaced with a worktree-scoped connection string. + * + * @param values - Settings to convert (not mutated). + * @param worktreeName - Active worktree label. + * @param plan - Which keys change and how. + * @returns A converted copy of `values`. + */ +export function convertSettingsForWorktree(values: SettingsRecord, worktreeName: string, plan: WorktreeConversionPlan): SettingsRecord { + const converted: SettingsRecord = { ...values }; + + for (const key of plan.urlKeys ?? []) { + const value = converted[key]; + if (typeof value === 'string') { + converted[key] = suffixUrlHostname(value, worktreeName); + } + } + + for (const key of plan.mongoKeys ?? []) { + const value = converted[key]; + if (typeof value === 'string') { + converted[key] = replaceUrlPort(value, getMongoPort(worktreeName)); + } + } + + if (plan.azuriteKeys?.length) { + const accountName = String(converted['STORAGE_ACCOUNT_NAME'] ?? ''); + const accountKey = String(converted['STORAGE_ACCOUNT_KEY'] ?? ''); + if (accountName && accountKey) { + const connectionString = buildAzuriteConnectionString({ + accountName, + accountKey, + ports: getAzuritePorts(worktreeName), + }); + for (const key of plan.azuriteKeys) { + converted[key] = connectionString; + } + } + } + + return converted; +} diff --git a/packages/cellix/local-dev/src/worktree/index.ts b/packages/cellix/local-dev/src/worktree/index.ts index 9a7fff5e7..b5023b551 100644 --- a/packages/cellix/local-dev/src/worktree/index.ts +++ b/packages/cellix/local-dev/src/worktree/index.ts @@ -1,4 +1,4 @@ -export { WorktreeJsonFileSync, type WorktreeJsonFileSyncOptions } from './json-file-sync.ts'; +export { convertSettingsForWorktree, type WorktreeConversionPlan } from './conversion.ts'; export { type AzuritePorts, buildAzuriteConnectionString, diff --git a/packages/cellix/local-dev/src/worktree/json-file-sync.ts b/packages/cellix/local-dev/src/worktree/json-file-sync.ts deleted file mode 100644 index a759bc61a..000000000 --- a/packages/cellix/local-dev/src/worktree/json-file-sync.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { existsSync } from 'node:fs'; -import { syncJsonFile } from '../files/json.ts'; -import { buildAzuriteConnectionString, getAzuritePorts } from './ports.ts'; -import { WorktreeSettings } from './settings.ts'; -import type { KnownWorktreeSettings, SettingsDocument, SettingsRecord, WorktreeSettingsOptions } from './types.ts'; -import { resolveWorktreeName } from './worktree-name.ts'; - -export interface WorktreeJsonFileSyncOptions extends WorktreeSettingsOptions { - /** Source JSON document to copy or transform. */ - sourcePath: string; - /** Destination path. Parent directories are created automatically. */ - targetPath: string; - /** Skip syncing when the source file is missing. Defaults to `false`. */ - skipIfMissing?: boolean; - /** Extra settings merged into the JSON document's `Values` object. */ - values?: SettingsRecord; - /** Keys that should receive a worktree-scoped Azurite connection string. */ - azuriteConnectionStringKeys?: string[]; -} - -/** - * Syncs a JSON settings file while applying generic worktree transforms. - * - * The helper is intentionally schema-light: it preserves the source document, - * merges caller-provided values into `Values`, and only applies generic URL, - * MongoDB, port, and optional Azurite connection-string transforms. - */ -export class WorktreeJsonFileSync { - private readonly options: WorktreeJsonFileSyncOptions; - - public constructor(options: WorktreeJsonFileSyncOptions) { - this.options = options; - } - - /** - * Copies or transforms the configured JSON source into the target path. - * - * @throws When the source file is missing and `skipIfMissing` is not true. - */ - public sync(): void { - if (this.options.skipIfMissing && !existsSync(this.options.sourcePath)) { - return; - } - - syncJsonFile({ - sourcePath: this.options.sourcePath, - targetPath: this.options.targetPath, - transform: (document) => this.transformDocument(document), - }); - } - - private transformDocument(document: SettingsDocument): SettingsDocument { - const env = this.options.env ?? process.env; - const worktreeName = resolveWorktreeName({ ...this.options, env }); - - const values: SettingsRecord = { - ...(document.Values ?? {}), - ...(this.options.values ?? {}), - }; - const transformedValues = new WorktreeSettings(worktreeName ? { env, worktreeName } : { env, worktree: false }).transformRecord(values) as KnownWorktreeSettings; - - if (worktreeName) { - this.applyAzuriteConnectionString(transformedValues, worktreeName); - } - - return { - ...document, - Values: transformedValues, - }; - } - - /** - * Replaces each configured key with a worktree-scoped Azurite connection - * string, when the document carries Azurite account credentials. - * - * @param values - Transformed settings to mutate in place. - * @param worktreeName - Active worktree used to derive Azurite ports. - */ - private applyAzuriteConnectionString(values: KnownWorktreeSettings, worktreeName: string): void { - const accountName = String(values.STORAGE_ACCOUNT_NAME ?? ''); - const accountKey = String(values.STORAGE_ACCOUNT_KEY ?? ''); - if (!accountName || !accountKey) { - return; - } - - const connectionString = buildAzuriteConnectionString({ - accountName, - accountKey, - ports: getAzuritePorts(worktreeName), - }); - for (const key of this.options.azuriteConnectionStringKeys ?? []) { - values[key] = connectionString; - } - } -} diff --git a/packages/cellix/local-dev/src/worktree/types.ts b/packages/cellix/local-dev/src/worktree/types.ts index a7694dcbd..eca3c9a77 100644 --- a/packages/cellix/local-dev/src/worktree/types.ts +++ b/packages/cellix/local-dev/src/worktree/types.ts @@ -15,15 +15,6 @@ export type KnownWorktreeSettings = SettingsRecord & { STORAGE_ACCOUNT_NAME?: unknown; }; -/** - * Azure Functions-style settings JSON document. - */ -export interface SettingsDocument { - /** Runtime values merged and transformed by local-dev helpers. */ - Values?: SettingsRecord; - [key: string]: unknown; -} - /** * Environment shape used by worktree helpers. */ diff --git a/packages/ocom/local-dev-config/src/api-settings/index.ts b/packages/ocom/local-dev-config/src/api-settings/index.ts index a940549ed..7e384205e 100644 --- a/packages/ocom/local-dev-config/src/api-settings/index.ts +++ b/packages/ocom/local-dev-config/src/api-settings/index.ts @@ -1,31 +1,61 @@ +import path from 'node:path'; import type { AzureFunctionsLocalSettingsOptions } from '@cellix/local-dev'; +import { readJsonFile } from '@cellix/local-dev/files'; +import { resolveWorkspaceRoot } from '@cellix/local-dev/workspace'; import type { OcomLocalDevOptions } from '../types.ts'; -import { buildOcomUrls } from '../urls/index.ts'; + +type ApiEnv = NodeJS.ProcessEnv & { + E2E?: string; +}; + +interface ApiSettingsDocument { + Values?: Record; + Host?: Record; +} + +function isE2E(env: NodeJS.ProcessEnv): boolean { + return ['1', 'true', 'yes'].includes(((env as ApiEnv).E2E ?? '').toLowerCase()); +} + +/** + * The exact api settings that get scoped to a worktree, named explicitly so it + * is obvious what changes. Everything else in the settings file is passed + * through untouched. The sync applies these only when a worktree is active. + */ +const API_WORKTREE_CONVERSION = { + // OIDC URLs: hostname gains the worktree suffix. + urlKeys: ['ACCOUNT_PORTAL_OIDC_ISSUER', 'ACCOUNT_PORTAL_OIDC_ENDPOINT', 'STAFF_PORTAL_OIDC_ISSUER', 'STAFF_PORTAL_OIDC_ENDPOINT'], + // Mongo connection string: port shifts to the worktree Mongo port. + mongoKeys: ['COSMOSDB_CONNECTION_STRING'], + // Azure storage: replaced with a worktree-scoped Azurite connection string. + azuriteKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], +} as const; /** - * Builds the OCOM api's Azure Functions local-settings policy. + * The single entry point for the OCOM api's Azure Functions local settings. * - * This is the single source of truth for the api's `local.settings.json` - * preparation, consumed by both the dev runner (`start-dev.ts`) and the - * standalone sync (`sync-local-settings.ts`). It maps the mock OIDC issuer and - * JWKS URLs into the E2E settings and names the keys that should receive a - * worktree-scoped Azurite connection string. + * The values themselves live in `apps/api/local.settings.json` (normal dev) and + * `apps/api/local-settings.e2e.json` (E2E); this reads the mode-appropriate file + * and declares — via {@link API_WORKTREE_CONVERSION} — exactly which keys the + * sync should scope to the active worktree. * - * @param options - Optional environment and workspace-root overrides forwarded - * to `buildOcomUrls`. - * @returns Local-settings options for `AzureFunctionsDevRunner` / - * `AzureFunctionsLocalSettings`. + * @param options - Optional environment (selects normal vs E2E) and + * workspace-root overrides. + * @returns Local-settings options for the api's dev runner and standalone sync. */ export function buildOcomApiLocalSettings(options: OcomLocalDevOptions = {}): AzureFunctionsLocalSettingsOptions { - const urls = buildOcomUrls(options); + const env = options.env ?? process.env; + const workspaceRoot = options.workspaceRoot ?? resolveWorkspaceRoot(); + const fileName = isE2E(env) ? 'local-settings.e2e.json' : 'local.settings.json'; + const document = readJsonFile(path.join(workspaceRoot, 'apps', 'api', fileName)); return { - e2eValues: { - ACCOUNT_PORTAL_OIDC_ISSUER: urls.mockCommunityAuthorityUrl, - ACCOUNT_PORTAL_OIDC_ENDPOINT: urls.mockCommunityJwksUrl, - STAFF_PORTAL_OIDC_ISSUER: urls.mockStaffAuthorityUrl, - STAFF_PORTAL_OIDC_ENDPOINT: urls.mockStaffJwksUrl, + ...(document.Values ? { values: document.Values } : {}), + ...(document.Host ? { host: document.Host } : {}), + worktreeConversion: { + urlKeys: [...API_WORKTREE_CONVERSION.urlKeys], + mongoKeys: [...API_WORKTREE_CONVERSION.mongoKeys], + azuriteKeys: [...API_WORKTREE_CONVERSION.azuriteKeys], }, - azuriteConnectionStringKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], }; } From 81b337a7aff35d2109bc44eaddff95b064e64410 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 16 Jun 2026 13:59:49 -0400 Subject: [PATCH 28/40] small adjustments for readability and stopped export of unused external modules --- .pnpm-store/v11/index.db | Bin 8192 -> 0 bytes .snyk | 35 + apps/docs/start-dev.ts | 4 +- package.json | 2 +- packages/cellix/local-dev/README.md | 43 +- packages/cellix/local-dev/manifest.md | 2 +- packages/cellix/local-dev/package.json | 8 - packages/cellix/local-dev/src/files/index.ts | 2 +- packages/cellix/local-dev/src/index.test.ts | 100 +- packages/cellix/local-dev/src/index.ts | 21 +- .../cellix/local-dev/src/process/index.ts | 4 + .../runners/azure-functions-local-settings.ts | 47 +- .../local-dev/src/runners/azure-functions.ts | 11 - .../cellix/local-dev/src/runners/azurite.ts | 75 +- .../local-dev/src/runners/docusaurus.ts | 10 - .../cellix/local-dev/src/runners/index.ts | 10 +- packages/cellix/local-dev/src/runners/node.ts | 27 - packages/cellix/local-dev/src/runners/vite.ts | 10 - packages/cellix/local-dev/src/vite/index.ts | 4 +- .../local-dev/src/worktree/conversion.ts | 30 +- .../cellix/local-dev/src/worktree/settings.ts | 24 +- packages/ocom/local-dev-config/README.md | 5 +- packages/ocom/local-dev-config/manifest.md | 1 + .../local-dev-config/src/hostnames/index.ts | 21 +- .../ocom/local-dev-config/vitest.config.ts | 2 - pnpm-lock.yaml | 1669 +++++++++-------- pnpm-workspace.yaml | 53 +- 27 files changed, 1193 insertions(+), 1027 deletions(-) delete mode 100644 .pnpm-store/v11/index.db diff --git a/.pnpm-store/v11/index.db b/.pnpm-store/v11/index.db deleted file mode 100644 index 044636a65ff67410fad601f0c39978108fefcea0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeIuzpBD86bA4#2p0s=&GDX11#$5OY&BppTCFMSqD0LV@%|C%po4=C;0rt5R!Xsx zd-*<+oFpepe$$EEhlalXPCq)NHmfksS%-)*#*-P9XRK%~B>T9;=Xc?(b$^tiS5|q+ zqJcmF0uX=z1Rwwb2tWV=5P$##awu^7v_7h}nsvK|di`yVdUMb_v)cb|%{g=6U0>Kr zkg^>qDAS^Pkk?+mi kUUFZI)hjuq$Cn@g0SG_<0uX=z1Rwwb2tWV=5J(070rxR8{{R30 diff --git a/.snyk b/.snyk index 922028793..733dfd7c4 100644 --- a/.snyk +++ b/.snyk @@ -111,3 +111,38 @@ ignore: reason: 'Transitive dependency in @docusaurus/mdx-loader and dev build tooling; no upgrade or patch available from upstream. Not exploitable in current usage (build-time image dimension parsing only).' expires: '2026-09-11T00:00:00.000Z' created: '2026-06-11T00:00:00.000Z' + 'SNYK-JS-AZUREIDENTITY-7964589': + - '* > @azure/identity@3.4.2': + reason: 'Transitive dependency via azurite>tedious; upgrade to 4.x is a breaking major-version change incompatible with azurite. Not exploitable in dev-only local storage emulation context.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-XML2JS-5414874': + - '* > xml2js@0.4.23': + reason: 'Transitive dependency via azurite>@azure/ms-rest-js; 0.4→0.5 is a breaking major change incompatible with @azure/ms-rest-js. Not exploitable in dev-only local storage emulation context.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-TOUGHCOOKIE-5672873': + - '* > tough-cookie@2.5.5': + reason: 'Transitive dependency via azurite>@azure/ms-rest-js; 2.x→4.x is a breaking major change. Not exploitable in dev-only local storage emulation context.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-OPENTELEMETRYCORE-17000000': + - '* > @opentelemetry/core': + reason: 'OTEL core 2.8.0+ requires sdk-node upgrade which has TypeScript compilation errors. Tracked alongside SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050 for resolution in the planned OTEL 2.x migration.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-BABELCORE-17342497': + - '* > @babel/core@7.29.0': + reason: 'No 7.29.x fix release exists; next release is major v8.0.0 which is a breaking change for all transitive consumers. Low severity, dev tooling only.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-JOI-17317897': + - '* > joi': + reason: 'No upgrade or patch available per Snyk; overridden to 17.13.4 which fixes the pnpm advisory. Accepting residual Snyk finding until a patched release is published.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' + 'SNYK-JS-JSYAML-17342520': + - '* > js-yaml@3.14.2': + reason: 'js-yaml 3.x→4.x is a breaking major change for all transitive consumers (gray-matter/Docusaurus chain). js-yaml@4.1.1 patched to 4.2.0 via pnpm override; 3.x path has no safe upgrade.' + expires: '2026-09-16T00:00:00.000Z' + created: '2026-06-16T00:00:00.000Z' diff --git a/apps/docs/start-dev.ts b/apps/docs/start-dev.ts index 7b8c1655c..6b14f748d 100644 --- a/apps/docs/start-dev.ts +++ b/apps/docs/start-dev.ts @@ -1,3 +1,3 @@ -import { runDocusaurusDev } from '@cellix/local-dev'; +import { DocusaurusDevRunner } from '@cellix/local-dev'; -runDocusaurusDev(); +new DocusaurusDevRunner().start(); diff --git a/package.json b/package.json index 8d13b475f..95f4f59f7 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "rimraf": "^6.0.1", "snyk": "^1.1300.2", "ts-scope-trimmer-plugin": "^1.0.2", - "turbo": "2.9.9", + "turbo": "2.9.14", "typescript": "catalog:", "vitest": "catalog:" }, diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index c6ed0e1fa..59a568456 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -36,9 +36,9 @@ In this monorepo, app packages consume the workspace package directly: Keep the app policy in a wrapper script and compose this package's generic helpers inside it: ```js -import { buildPortlessUrl, WorktreeNodeDevRunner } from '@cellix/local-dev'; +import { buildPortlessUrl, NodeDevRunner } from '@cellix/local-dev'; -new WorktreeNodeDevRunner({ +new NodeDevRunner({ settings: { BASE_URL: buildPortlessUrl('mock-auth.example.localhost'), PORT: '50000', @@ -46,32 +46,38 @@ new WorktreeNodeDevRunner({ }).start(); ``` -For settings files, let the app decide the keys and values. The shared syncer applies worktree URL suffixes, Mongo port offsets, and requested Azurite connection strings: +For settings files, let the app decide the document shape and any app-owned transform: ```js -import { WorktreeJsonFileSync } from '@cellix/local-dev'; +import { syncJsonFile } from '@cellix/local-dev'; -new WorktreeJsonFileSync({ +syncJsonFile({ sourcePath: 'local-settings.e2e.json', targetPath: 'deploy/local.settings.json', - values: { - ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.example.localhost:1355/community', - }, - azuriteConnectionStringKeys: ['AzureWebJobsStorage'], -}).sync(); + transform: (document) => ({ + ...document, + Values: { + ...document.Values, + MODE: 'e2e', + }, + }), +}); ``` -Azure Functions can do this directly in the runner so `func start` sees the prepared `local.settings.json` in its script root: +Azure Functions can sync worktree-aware values directly in the runner so `func start` sees the prepared `local.settings.json` in its script root: ```js -import { WorktreeAzureFunctionsDevRunner } from '@cellix/local-dev'; +import { AzureFunctionsDevRunner } from '@cellix/local-dev'; -new WorktreeAzureFunctionsDevRunner({ +new AzureFunctionsDevRunner({ localSettings: { e2eValues: { ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.example.localhost:1355/community', }, - azuriteConnectionStringKeys: ['AzureWebJobsStorage'], + worktreeConversion: { + urlKeys: ['ACCOUNT_PORTAL_OIDC_ISSUER'], + azuriteKeys: ['AzureWebJobsStorage'], + }, }, }).start(); ``` @@ -116,13 +122,10 @@ also published for consumers that want narrower imports: - `NodeDevRunner` - `AzuriteDevRunner` - `WorktreeSettings` -- `WorktreeViteDevRunner` -- `WorktreeAzureFunctionsDevRunner` -- `WorktreeAzureFunctionsLocalSettings` +- `AzureFunctionsLocalSettings` - `WorktreeMode` -- `WorktreeNodeDevRunner` -- `WorktreeAzuriteDevRunner` -- `WorktreeJsonFileSync` +- `convertSettingsForWorktree` +- `WorktreeConversionPlan` - `getWorktreePortOffset` - `getMongoPort` - `getAzuritePorts` diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index 567f064b5..941555993 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -38,7 +38,7 @@ Root entrypoint exports: - `isGracefulInterruptExit(signal, code)`, `forwardChildExit(child)` - `getWorktreePortOffset(worktreeName?)`, `getMongoPort(worktreeName?)`, `getAzuritePorts(worktreeName?)`, `buildAzuriteConnectionString(options)` - `ViteDevRunner`, `DocusaurusDevRunner`, `AzureFunctionsDevRunner`, `NodeDevRunner`, `AzuriteDevRunner` -- `WorktreeSettings`, `WorktreeViteDevRunner`, `WorktreeAzureFunctionsDevRunner`, `WorktreeAzureFunctionsLocalSettings`, `WorktreeNodeDevRunner`, `WorktreeAzuriteDevRunner`, `WorktreeJsonFileSync`, `WorktreeMode` +- `WorktreeSettings`, `AzureFunctionsLocalSettings`, `convertSettingsForWorktree`, `WorktreeConversionPlan`, `WorktreeMode` - `runViteDev(options?)`, `runDocusaurusDev(options?)`, `runAzureFunctionsDev(options?)`, `runNodeDev(options?)`, `runAzuriteDev(options)` - `runTsxDev(options?)` remains as a deprecated compatibility alias for `runNodeDev(options?)` diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json index 68ebde17a..43addae60 100644 --- a/packages/cellix/local-dev/package.json +++ b/packages/cellix/local-dev/package.json @@ -18,10 +18,6 @@ "types": "./dist/files/index.d.ts", "default": "./dist/files/index.js" }, - "./process": { - "types": "./dist/process/index.d.ts", - "default": "./dist/process/index.js" - }, "./runners": { "types": "./dist/runners/index.d.ts", "default": "./dist/runners/index.js" @@ -30,10 +26,6 @@ "types": "./dist/urls/index.d.ts", "default": "./dist/urls/index.js" }, - "./vite": { - "types": "./dist/vite/index.d.ts", - "default": "./dist/vite/index.js" - }, "./workspace": { "types": "./dist/workspace/index.d.ts", "default": "./dist/workspace/index.js" diff --git a/packages/cellix/local-dev/src/files/index.ts b/packages/cellix/local-dev/src/files/index.ts index 6a3484aee..0eca37078 100644 --- a/packages/cellix/local-dev/src/files/index.ts +++ b/packages/cellix/local-dev/src/files/index.ts @@ -1,2 +1,2 @@ export { type DotEnvValues, readDotEnv } from './dotenv.ts'; -export { readJsonFile, type SyncJsonFileOptions, syncJsonFile, writeJsonFile } from './json.ts'; +export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './json.ts'; diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 4c02bad11..a8a8959eb 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -8,7 +8,6 @@ import { applyWorktreeSuffix, buildAzuriteConnectionString, buildPortlessUrl, - buildViteArgs, convertSettingsForWorktree, getAzuritePorts, getMongoPort, @@ -25,7 +24,8 @@ import { ViteDevRunner, WorktreeSettings, } from '@cellix/local-dev'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { buildViteArgs } from './vite/index.ts'; type DotEnvFixtureValues = Record & { API_URL?: string; @@ -40,7 +40,24 @@ function createWorkspaceFixture(): string { return root; } +function createMockChildProcess(): ReturnType { + const proc = new EventEmitter() as ReturnType; + proc.kill = vi.fn(() => true); + return proc; +} + +function expectNoDirectProcessExit(): ReturnType { + return vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit should not be called by embeddable runners'); + }) as never); +} + describe('@cellix/local-dev', () => { + afterEach(() => { + vi.restoreAllMocks(); + process.exitCode = undefined; + }); + it('resolves the workspace root from a nested directory', () => { const workspaceRoot = createWorkspaceFixture(); const nestedDir = path.join(workspaceRoot, 'fixtures'); @@ -260,10 +277,82 @@ describe('@cellix/local-dev', () => { blobPort: getAzuritePorts('Jason/Feature 123').blob, queuePort: getAzuritePorts('Jason/Feature 123').queue, tablePort: getAzuritePorts('Jason/Feature 123').table, - blobLocation: path.join(workspaceRoot, '__blobstorage__-Jason/Feature 123'), + blobLocation: path.join(workspaceRoot, '__blobstorage__-jason-feature-123'), }); }); + it('sets exitCode instead of terminating the parent process when Azurite spawn fails', () => { + const children: ReturnType[] = []; + const spawn: RunnerSpawn = () => { + const child = createMockChildProcess(); + children.push(child); + return child; + }; + const exitSpy = expectNoDirectProcessExit(); + const listenerCountBeforeStart = process.listenerCount('SIGINT'); + + new AzuriteDevRunner({ spawn }).start(); + children[1]?.emit('error', new Error('missing binary')); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(1); + expect(process.listenerCount('SIGINT')).toBe(listenerCountBeforeStart); + expect(children.map((child) => child.kill)).toEqual([expect.any(Function), expect.any(Function), expect.any(Function)]); + expect(children.every((child) => vi.mocked(child.kill).mock.calls.length === 1)).toBe(true); + }); + + it('sets exitCode instead of terminating the parent process when Azurite exits unexpectedly', () => { + const children: ReturnType[] = []; + const spawn: RunnerSpawn = () => { + const child = createMockChildProcess(); + children.push(child); + return child; + }; + const exitSpy = expectNoDirectProcessExit(); + const listenerCountBeforeStart = process.listenerCount('SIGTERM'); + + new AzuriteDevRunner({ spawn }).start(); + children[0]?.emit('exit', 17, null); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(17); + expect(process.listenerCount('SIGTERM')).toBe(listenerCountBeforeStart); + expect(children.every((child) => vi.mocked(child.kill).mock.calls.length === 1)).toBe(true); + }); + + it('sets a successful exitCode after all Azurite processes stop gracefully', () => { + const children: ReturnType[] = []; + const spawn: RunnerSpawn = () => { + const child = createMockChildProcess(); + children.push(child); + return child; + }; + const exitSpy = expectNoDirectProcessExit(); + + new AzuriteDevRunner({ spawn }).start(); + for (const child of children) { + child.emit('exit', 130, 'SIGINT'); + } + + expect(exitSpy).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(0); + }); + + it('preserves an existing non-zero exitCode when Azurite later reports another failure', () => { + const children: ReturnType[] = []; + const spawn: RunnerSpawn = () => { + const child = createMockChildProcess(); + children.push(child); + return child; + }; + process.exitCode = 9; + + new AzuriteDevRunner({ spawn }).start(); + children[2]?.emit('exit', 17, null); + + expect(process.exitCode).toBe(9); + }); + it('converts only the named settings for a worktree, leaving the rest untouched', () => { const converted = convertSettingsForWorktree( { @@ -282,6 +371,9 @@ describe('@cellix/local-dev', () => { azuriteKeys: ['AZURE_STORAGE_CONNECTION_STRING', 'AzureWebJobsStorage'], }, ); + const typedConverted = converted as typeof converted & { + COSMOSDB_CONNECTION_STRING: string; + }; expect(converted).toMatchObject({ ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', @@ -291,7 +383,7 @@ describe('@cellix/local-dev', () => { COSMOSDB_DBNAME: 'ocom', STORAGE_ACCOUNT_NAME: 'devstoreaccount1', }); - expect(converted['COSMOSDB_CONNECTION_STRING']).toBe(`mongodb://127.0.0.1:${getMongoPort('Jason/Feature 123')}/ocom`); + expect(typedConverted.COSMOSDB_CONNECTION_STRING).toBe(`mongodb://127.0.0.1:${getMongoPort('Jason/Feature 123')}/ocom`); }); it('generates Azure Functions local settings before starting func', () => { diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index c038ec91e..eba85b0d6 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,14 +1,5 @@ -export { - type DotEnvValues, - readDotEnv, -} from './files/dotenv.ts'; -export { - readJsonFile, - type SyncJsonFileOptions, - syncJsonFile, - writeJsonFile, -} from './files/json.ts'; -export { forwardChildExit, isGracefulInterruptExit } from './process/index.ts'; +export { type DotEnvValues, readDotEnv } from './files/dotenv.ts'; +export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './files/json.ts'; export { type AzureFunctionsDevOptions, AzureFunctionsDevRunner, @@ -23,13 +14,6 @@ export { type ResolvedAzuriteOptions, type RunnerOptions, type RunnerSpawn, - runAzureFunctionsDev, - runAzuriteDev, - runDocusaurusDev, - runNodeDev, - runTsxDev, - runViteDev, - type TsxRunnerOptions, type ViteDevOptions, ViteDevRunner, } from './runners/index.ts'; @@ -41,7 +25,6 @@ export { replaceUrlPort, sanitizeWorktreeHostnameLabel, } from './urls/index.ts'; -export { type BuildViteArgsOptions, buildViteArgs, isE2E } from './vite/index.ts'; export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace/index.ts'; export { convertSettingsForWorktree, diff --git a/packages/cellix/local-dev/src/process/index.ts b/packages/cellix/local-dev/src/process/index.ts index ec9394fca..08177cc5f 100644 --- a/packages/cellix/local-dev/src/process/index.ts +++ b/packages/cellix/local-dev/src/process/index.ts @@ -11,6 +11,10 @@ export function isGracefulInterruptExit(signal: NodeJS.Signals | null | undefine /** * Forwards a spawned child process exit code back to the parent process while * treating common interrupt exits as a successful shutdown. + * + * This helper sets `process.exitCode` instead of calling `process.exit()`, so a + * wrapper script can finish naturally and embedders do not lose control of the + * current Node process. */ export function forwardChildExit(child: ChildProcess): void { child.on('exit', (code, signal) => { diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts index 161236fd8..f5bf3db50 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -12,6 +12,31 @@ function isE2E(env: NodeJS.ProcessEnv): boolean { return ['1', 'true', 'yes'].includes(((env as LocalSettingsEnv).E2E ?? '').toLowerCase()); } +function resolveTargetPath(options: AzureFunctionsLocalSettingsOptions): string { + const { appDir = process.cwd(), scriptRoot = 'deploy/' } = options; + return options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'); +} + +function resolveSettingsWorktreeName(options: AzureFunctionsLocalSettingsOptions, env: NodeJS.ProcessEnv): string | undefined { + return resolveWorktreeName({ + env, + ...(typeof options.worktree === 'boolean' ? { worktree: options.worktree } : {}), + ...(options.worktreeName ? { worktreeName: options.worktreeName } : {}), + }); +} + +function buildModeValues(options: AzureFunctionsLocalSettingsOptions, env: NodeJS.ProcessEnv): SettingsRecord { + const values: SettingsRecord = { ...options.values }; + if (isE2E(env) && options.e2eValues) { + Object.assign(values, options.e2eValues); + } + return values; +} + +function applyWorktreeConversion(values: SettingsRecord, worktreeName: string | undefined, plan: WorktreeConversionPlan | undefined): SettingsRecord { + return worktreeName && plan ? convertSettingsForWorktree(values, worktreeName, plan) : values; +} + export interface AzureFunctionsLocalSettingsOptions { /** App directory used to resolve the default target path. Defaults to `process.cwd()`. */ appDir?: string; @@ -53,24 +78,12 @@ export class AzureFunctionsLocalSettings { * Writes the worktree-scoped settings into the Functions script root. */ public sync(): void { - const { appDir = process.cwd(), scriptRoot = 'deploy/', env = process.env } = this.options; - const targetPath = this.options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'); - const worktreeName = resolveWorktreeName({ - env, - ...(typeof this.options.worktree === 'boolean' ? { worktree: this.options.worktree } : {}), - ...(this.options.worktreeName ? { worktreeName: this.options.worktreeName } : {}), - }); - - // 1. LOAD — the settings for this mode (base, plus E2E overrides in E2E). - const values: SettingsRecord = { ...this.options.values }; - if (isE2E(env) && this.options.e2eValues) { - Object.assign(values, this.options.e2eValues); - } - - // 2. CONVERT — scope the caller-named keys to the active worktree (skipped otherwise). - const converted = worktreeName && this.options.worktreeConversion ? convertSettingsForWorktree(values, worktreeName, this.options.worktreeConversion) : values; + const env = this.options.env ?? process.env; + const targetPath = resolveTargetPath(this.options); + const worktreeName = resolveSettingsWorktreeName(this.options, env); + const values = buildModeValues(this.options, env); + const converted = applyWorktreeConversion(values, worktreeName, this.options.worktreeConversion); - // 3. WRITE — emit local.settings.json for the Functions host. writeJsonFile(targetPath, { IsEncrypted: false, Values: converted, diff --git a/packages/cellix/local-dev/src/runners/azure-functions.ts b/packages/cellix/local-dev/src/runners/azure-functions.ts index 1ed065557..eacf92a64 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions.ts @@ -110,14 +110,3 @@ export class AzureFunctionsDevRunner { return child; } } - -/** - * Starts an Azure Functions dev process. - * - * @param options - Functions host options, worktree context, env, and spawn - * override. - * @returns The spawned Functions host process. - */ -export function runAzureFunctionsDev(options: AzureFunctionsDevOptions = {}): ChildProcess { - return new AzureFunctionsDevRunner(options).start(); -} diff --git a/packages/cellix/local-dev/src/runners/azurite.ts b/packages/cellix/local-dev/src/runners/azurite.ts index 6ed7736d9..53ccbf096 100644 --- a/packages/cellix/local-dev/src/runners/azurite.ts +++ b/packages/cellix/local-dev/src/runners/azurite.ts @@ -1,12 +1,24 @@ import type { ChildProcess } from 'node:child_process'; import path from 'node:path'; import { isGracefulInterruptExit } from '../process/index.ts'; +import { sanitizeWorktreeHostnameLabel } from '../urls/index.ts'; import { resolveWorkspaceRoot } from '../workspace/index.ts'; import { getAzuritePorts } from '../worktree/ports.ts'; import { resolveWorktreeName } from '../worktree/worktree-name.ts'; import { spawnInherited } from './spawn.ts'; import type { RunnerOptions } from './types.ts'; +function setProcessExitCode(code: number): void { + if (process.exitCode === undefined || process.exitCode === 0) { + process.exitCode = code; + } +} + +function getWorktreeStorageSuffix(worktreeName: string | undefined): string { + const safeLabel = sanitizeWorktreeHostnameLabel(worktreeName); + return safeLabel ? `-${safeLabel}` : ''; +} + export interface AzuriteDevOptions extends RunnerOptions { /** Workspace root used for storage directories. Defaults to auto-discovery. */ workspaceRoot?: string; @@ -44,8 +56,8 @@ export interface ResolvedAzuriteOptions { * * When no worktree is active the runner falls back to Azurite's default ports * and unsuffixed storage directories. All three services inherit stdio; if one - * exits unexpectedly the runner terminates the others and forwards a non-zero - * exit code. + * exits unexpectedly the runner terminates the others and sets the parent + * process exit code for CLI callers without forcing embedders to exit. */ export class AzuriteDevRunner { private readonly options: AzuriteDevOptions; @@ -64,7 +76,7 @@ export class AzuriteDevRunner { const worktreeName = resolveWorktreeName(this.options); const workspaceRoot = this.options.workspaceRoot ?? resolveWorkspaceRoot(); const ports = getAzuritePorts(worktreeName); - const storageSuffix = worktreeName ? `-${worktreeName}` : ''; + const storageSuffix = getWorktreeStorageSuffix(worktreeName); const directories = this.options.storageDirectories ?? {}; return { @@ -85,23 +97,35 @@ export class AzuriteDevRunner { public start(): ChildProcess[] { const resolved = this.resolveOptions(); const silent = this.options.silent ?? true; - const procSpecs: Array<[string, string[]]> = [ + const azuriteProcesses: Array<[string, string[]]> = [ ['azurite-blob', [...(silent ? ['--silent'] : []), '--blobPort', String(resolved.blobPort), '--location', path.resolve(resolved.blobLocation)]], ['azurite-queue', [...(silent ? ['--silent'] : []), '--queuePort', String(resolved.queuePort), '--location', path.resolve(resolved.queueLocation)]], ['azurite-table', [...(silent ? ['--silent'] : []), '--tablePort', String(resolved.tablePort), '--location', path.resolve(resolved.tableLocation)]], ]; - const procs = procSpecs.map(([command, args]) => { + let procs: ChildProcess[] = []; + const stopAzuriteProcesses = (signal?: NodeJS.Signals) => { + for (const proc of procs) { + proc.kill(signal); + } + }; + const stopOnSigint = () => stopAzuriteProcesses('SIGINT'); + const stopOnSigterm = () => stopAzuriteProcesses('SIGTERM'); + const removeSignalHandlers = () => { + process.off('SIGINT', stopOnSigint); + process.off('SIGTERM', stopOnSigterm); + }; + + procs = azuriteProcesses.map(([command, args]) => { const proc = spawnInherited(command, args, { ...(this.options.env ? { env: this.options.env } : {}), ...(this.options.spawn ? { spawn: this.options.spawn } : {}), }); proc.on('error', (error) => { console.error(`[azurite] failed to start ${command}: ${error.message}`); - for (const runningProc of procs) { - runningProc.kill(); - } - process.exit(1); + stopAzuriteProcesses(); + removeSignalHandlers(); + setProcessExitCode(1); }); return proc; }); @@ -113,41 +137,22 @@ export class AzuriteDevRunner { proc.on('exit', (code, signal) => { if (isGracefulInterruptExit(signal, code)) { if (++exited === procs.length) { - process.exit(0); + removeSignalHandlers(); + setProcessExitCode(0); } return; } console.error(`[azurite] process exited unexpectedly: code=${code} signal=${signal}`); - for (const runningProc of procs) { - runningProc.kill(); - } - process.exit(code ?? 1); + stopAzuriteProcesses(); + removeSignalHandlers(); + setProcessExitCode(code ?? 1); }); } - process.on('SIGINT', () => { - for (const proc of procs) { - proc.kill('SIGINT'); - } - }); - process.on('SIGTERM', () => { - for (const proc of procs) { - proc.kill('SIGTERM'); - } - }); + process.on('SIGINT', stopOnSigint); + process.on('SIGTERM', stopOnSigterm); return procs; } } - -/** - * Starts Azurite blob, queue, and table services on worktree-scoped ports. - * - * @param options - Optional workspace root, storage directories, worktree - * context, env, and spawn override. - * @returns The spawned Azurite child processes. - */ -export function runAzuriteDev(options: AzuriteDevOptions = {}): ChildProcess[] { - return new AzuriteDevRunner(options).start(); -} diff --git a/packages/cellix/local-dev/src/runners/docusaurus.ts b/packages/cellix/local-dev/src/runners/docusaurus.ts index dcbf1eb60..12e75ac89 100644 --- a/packages/cellix/local-dev/src/runners/docusaurus.ts +++ b/packages/cellix/local-dev/src/runners/docusaurus.ts @@ -35,13 +35,3 @@ export class DocusaurusDevRunner { return child; } } - -/** - * Starts the Docusaurus dev server. - * - * @param options - Optional env and spawn override. - * @returns The spawned child process. - */ -export function runDocusaurusDev(options: RunnerOptions = {}): ChildProcess { - return new DocusaurusDevRunner(options).start(); -} diff --git a/packages/cellix/local-dev/src/runners/index.ts b/packages/cellix/local-dev/src/runners/index.ts index 4a360b9d0..bc160f5c1 100644 --- a/packages/cellix/local-dev/src/runners/index.ts +++ b/packages/cellix/local-dev/src/runners/index.ts @@ -1,7 +1,7 @@ -export { type AzureFunctionsDevOptions, AzureFunctionsDevRunner, runAzureFunctionsDev } from './azure-functions.ts'; +export { type AzureFunctionsDevOptions, AzureFunctionsDevRunner } from './azure-functions.ts'; export { AzureFunctionsLocalSettings, type AzureFunctionsLocalSettingsOptions } from './azure-functions-local-settings.ts'; -export { type AzuriteDevOptions, AzuriteDevRunner, type ResolvedAzuriteOptions, runAzuriteDev } from './azurite.ts'; -export { DocusaurusDevRunner, runDocusaurusDev } from './docusaurus.ts'; -export { type NodeDevOptions, NodeDevRunner, runNodeDev, runTsxDev, type TsxRunnerOptions } from './node.ts'; +export { type AzuriteDevOptions, AzuriteDevRunner, type ResolvedAzuriteOptions } from './azurite.ts'; +export { DocusaurusDevRunner } from './docusaurus.ts'; +export { type NodeDevOptions, NodeDevRunner } from './node.ts'; export type { EnvRunnerOptions, RunnerOptions, RunnerSpawn } from './types.ts'; -export { runViteDev, type ViteDevOptions, ViteDevRunner } from './vite.ts'; +export { type ViteDevOptions, ViteDevRunner } from './vite.ts'; diff --git a/packages/cellix/local-dev/src/runners/node.ts b/packages/cellix/local-dev/src/runners/node.ts index 4f8d297df..905c9a41a 100644 --- a/packages/cellix/local-dev/src/runners/node.ts +++ b/packages/cellix/local-dev/src/runners/node.ts @@ -42,30 +42,3 @@ export class NodeDevRunner { return child; } } - -/** - * Starts a Node-backed dev process. - * - * @param options - Optional entrypoint, Node arguments, env, and spawn - * override. - * @returns The spawned child process. - */ -export function runNodeDev(options: NodeDevOptions = {}): ChildProcess { - return new NodeDevRunner(options).start(); -} - -/** - * @deprecated Use `NodeDevRunner` or `runNodeDev`. TSX is no longer required - * for local dev entrypoints on the supported Node runtime. - */ -export type TsxRunnerOptions = NodeDevOptions; - -/** - * @deprecated Use `runNodeDev`. - * - * @param options - Node dev options preserved for compatibility. - * @returns The spawned child process. - */ -export function runTsxDev(options: TsxRunnerOptions = {}): ChildProcess { - return runNodeDev(options); -} diff --git a/packages/cellix/local-dev/src/runners/vite.ts b/packages/cellix/local-dev/src/runners/vite.ts index a4a72c1c5..086d4f219 100644 --- a/packages/cellix/local-dev/src/runners/vite.ts +++ b/packages/cellix/local-dev/src/runners/vite.ts @@ -46,13 +46,3 @@ export class ViteDevRunner { return child; } } - -/** - * Starts a Vite dev process. - * - * @param options - Optional env and spawn override. - * @returns The spawned child process. - */ -export function runViteDev(options: ViteDevOptions = {}): ChildProcess { - return new ViteDevRunner(options).start(); -} diff --git a/packages/cellix/local-dev/src/vite/index.ts b/packages/cellix/local-dev/src/vite/index.ts index f50bd78b0..3adff7888 100644 --- a/packages/cellix/local-dev/src/vite/index.ts +++ b/packages/cellix/local-dev/src/vite/index.ts @@ -1,4 +1,4 @@ -export interface BuildViteArgsOptions { +interface BuildViteArgsOptions { /** Host passed to Vite. Defaults to `127.0.0.1`. */ host?: string; /** Optional port passed to Vite. */ @@ -19,7 +19,7 @@ type ViteEnv = NodeJS.ProcessEnv & { * @param env - Environment to inspect. Defaults to `process.env`. * @returns Whether `E2E` is enabled with a truthy local-dev value. */ -export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { +function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { const viteEnv = env as ViteEnv; return ['1', 'true', 'yes'].includes((viteEnv.E2E ?? '').toLowerCase()); } diff --git a/packages/cellix/local-dev/src/worktree/conversion.ts b/packages/cellix/local-dev/src/worktree/conversion.ts index 7a2db1c9c..29f3a562a 100644 --- a/packages/cellix/local-dev/src/worktree/conversion.ts +++ b/packages/cellix/local-dev/src/worktree/conversion.ts @@ -15,7 +15,9 @@ export interface WorktreeConversionPlan { mongoKeys?: string[]; /** * Keys that receive a worktree-scoped Azurite connection string, built from - * the document's `STORAGE_ACCOUNT_NAME` / `STORAGE_ACCOUNT_KEY`. + * the same settings record's `STORAGE_ACCOUNT_NAME` and + * `STORAGE_ACCOUNT_KEY` values. When either credential is missing, these keys + * are left untouched. */ azuriteKeys?: string[]; } @@ -39,6 +41,22 @@ function suffixUrlHostname(value: string, worktreeName: string): string { } } +function buildWorktreeAzuriteConnectionString(values: SettingsRecord, worktreeName: string): string | undefined { + // biome-ignore lint/complexity/useLiteralKeys: noPropertyAccessFromIndexSignature requires bracket notation + const accountName = String(values['STORAGE_ACCOUNT_NAME'] ?? ''); + // biome-ignore lint/complexity/useLiteralKeys: noPropertyAccessFromIndexSignature requires bracket notation + const accountKey = String(values['STORAGE_ACCOUNT_KEY'] ?? ''); + if (!accountName || !accountKey) { + return undefined; + } + + return buildAzuriteConnectionString({ + accountName, + accountKey, + ports: getAzuritePorts(worktreeName), + }); +} + /** * Returns a copy of `values` with the worktree conversion applied to exactly the * keys named in `plan` — URL hostnames suffixed, Mongo ports shifted, and @@ -67,14 +85,8 @@ export function convertSettingsForWorktree(values: SettingsRecord, worktreeName: } if (plan.azuriteKeys?.length) { - const accountName = String(converted['STORAGE_ACCOUNT_NAME'] ?? ''); - const accountKey = String(converted['STORAGE_ACCOUNT_KEY'] ?? ''); - if (accountName && accountKey) { - const connectionString = buildAzuriteConnectionString({ - accountName, - accountKey, - ports: getAzuritePorts(worktreeName), - }); + const connectionString = buildWorktreeAzuriteConnectionString(converted, worktreeName); + if (connectionString) { for (const key of plan.azuriteKeys) { converted[key] = connectionString; } diff --git a/packages/cellix/local-dev/src/worktree/settings.ts b/packages/cellix/local-dev/src/worktree/settings.ts index 3c677b5f3..6af356e6b 100644 --- a/packages/cellix/local-dev/src/worktree/settings.ts +++ b/packages/cellix/local-dev/src/worktree/settings.ts @@ -3,6 +3,9 @@ import { getMongoPort, getWorktreePortOffset } from './ports.ts'; import type { KnownWorktreeSettings, SettingsRecord, WorktreeSettingsOptions } from './types.ts'; import { resolveWorktreeName } from './worktree-name.ts'; +const HTTP_URL_PATTERN = /https?:\/\/[^\s;'"<>]+/g; +const MONGO_URL_PATTERN = /mongodb:\/\/[^\s;'"<>]+/g; + function isPlainRecord(value: unknown): value is SettingsRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -13,6 +16,10 @@ function stringifyEnvValue(value: unknown): string | undefined { return undefined; } +function preservesBareOriginShape(originalValue: string, parsedUrl: URL): boolean { + return !originalValue.endsWith('/') && parsedUrl.pathname === '/' && !parsedUrl.search && !parsedUrl.hash; +} + function transformUrl(urlValue: string, worktreeName: string | undefined): string { try { const url = new URL(urlValue); @@ -25,7 +32,7 @@ function transformUrl(urlValue: string, worktreeName: string | undefined): strin } const transformedUrl = url.toString(); - if (!urlValue.endsWith('/') && url.pathname === '/' && !url.search && !url.hash) { + if (preservesBareOriginShape(urlValue, url)) { return transformedUrl.slice(0, -1); } @@ -36,8 +43,8 @@ function transformUrl(urlValue: string, worktreeName: string | undefined): strin } function transformStringValue(value: string, worktreeName: string | undefined): string { - const withHttpUrls = value.replace(/https?:\/\/[^\s;'"<>]+/g, (match) => transformUrl(match, worktreeName)); - return withHttpUrls.replace(/mongodb:\/\/[^\s;'"<>]+/g, (match) => transformUrl(match, worktreeName)); + const withHttpUrls = value.replace(HTTP_URL_PATTERN, (match) => transformUrl(match, worktreeName)); + return withHttpUrls.replace(MONGO_URL_PATTERN, (match) => transformUrl(match, worktreeName)); } function transformValue(value: unknown, worktreeName: string | undefined): unknown { @@ -62,6 +69,17 @@ function transformValue(value: unknown, worktreeName: string | undefined): unkno * * URL-like strings receive `.localhost` hostname suffixes, MongoDB URLs receive * worktree-specific ports, and `PORT` receives the generic worktree port offset. + * + * @example + * ```ts + * new WorktreeSettings({ + * env: { WORKTREE_NAME: 'feature-a' }, + * settings: { + * BASE_URL: 'https://ownercommunity.localhost:1355', + * COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', + * }, + * }).toEnv(); + * ``` */ export class WorktreeSettings { private readonly env: NodeJS.ProcessEnv; diff --git a/packages/ocom/local-dev-config/README.md b/packages/ocom/local-dev-config/README.md index fa6a18c24..fd5832b66 100644 --- a/packages/ocom/local-dev-config/README.md +++ b/packages/ocom/local-dev-config/README.md @@ -19,12 +19,12 @@ This package owns Owner Community application policy: which app `.env` files def Use this package as the OCOM source of local URLs, then pass those values into the generic Cellix worktree runners: ```ts -import { WorktreeViteDevRunner } from '@cellix/local-dev'; +import { ViteDevRunner } from '@cellix/local-dev'; import { buildOcomUrls } from '@ocom/local-dev-config'; const urls = buildOcomUrls(); -new WorktreeViteDevRunner({ +new ViteDevRunner({ settings: { VITE_APP_UI_COMMUNITY_BASE_URL: urls.uiCommunityBaseUrl, VITE_COMMON_API_ENDPOINT: urls.apiGraphqlUrl, @@ -42,6 +42,7 @@ also published for narrower imports: - `getOcomHostnames(options?)` - `buildOcomUrls(options?)` +- `buildOcomApiLocalSettings(options?)` - `OcomLocalDevOptions` - `OcomHostnames` - `OcomUrls` diff --git a/packages/ocom/local-dev-config/manifest.md b/packages/ocom/local-dev-config/manifest.md index 583917937..506060345 100644 --- a/packages/ocom/local-dev-config/manifest.md +++ b/packages/ocom/local-dev-config/manifest.md @@ -27,6 +27,7 @@ Root entrypoint exports: - `getOcomHostnames(options?)` - `buildOcomUrls(options?)` +- `buildOcomApiLocalSettings(options?)` - `OcomLocalDevOptions` - `OcomHostnames` - `OcomUrls` diff --git a/packages/ocom/local-dev-config/src/hostnames/index.ts b/packages/ocom/local-dev-config/src/hostnames/index.ts index f55981f2a..0ea56d6cf 100644 --- a/packages/ocom/local-dev-config/src/hostnames/index.ts +++ b/packages/ocom/local-dev-config/src/hostnames/index.ts @@ -26,6 +26,14 @@ function readAppEnv(workspaceRoot: string, appName: string): DotEnvValues & Ocom return readDotEnv(path.join(workspaceRoot, 'apps', appName, '.env')) as DotEnvValues & OcomEnvValues; } +function firstDefinedUrl(...values: Array): string { + return values.find((value) => value !== undefined) ?? ''; +} + +function requiredHostnameFromSources(key: string, ...values: Array): string { + return requiredHostname(firstDefinedUrl(...values), key); +} + /** * Resolves the OCOM local-development hostnames from app `.env` files and * optional process environment overrides. @@ -53,12 +61,15 @@ export function getOcomHostnames(options: OcomLocalDevOptions = {}): OcomHostnam const communityEnv = readAppEnv(workspaceRoot, 'ui-community'); const staffEnv = readAppEnv(workspaceRoot, 'ui-staff'); const worktreeName = env.WORKTREE_NAME; - const communityHostname = requiredHostname(env.VITE_APP_UI_COMMUNITY_BASE_URL ?? communityEnv.VITE_APP_UI_COMMUNITY_BASE_URL ?? '', 'VITE_APP_UI_COMMUNITY_BASE_URL'); - const apiHostname = requiredHostname(env.VITE_COMMON_API_ENDPOINT ?? communityEnv.VITE_COMMON_API_ENDPOINT ?? '', 'VITE_COMMON_API_ENDPOINT'); - const mockAuthHostname = requiredHostname(env.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY ?? communityEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY ?? '', 'VITE_APP_UI_COMMUNITY_B2C_AUTHORITY'); - const staffHostname = requiredHostname( - env.VITE_APP_UI_STAFF_BASE_URL ?? staffEnv.VITE_APP_UI_STAFF_BASE_URL ?? env.VITE_APP_UI_STAFF_AAD_REDIRECT_URI ?? staffEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI ?? '', + const communityHostname = requiredHostnameFromSources('VITE_APP_UI_COMMUNITY_BASE_URL', env.VITE_APP_UI_COMMUNITY_BASE_URL, communityEnv.VITE_APP_UI_COMMUNITY_BASE_URL); + const apiHostname = requiredHostnameFromSources('VITE_COMMON_API_ENDPOINT', env.VITE_COMMON_API_ENDPOINT, communityEnv.VITE_COMMON_API_ENDPOINT); + const mockAuthHostname = requiredHostnameFromSources('VITE_APP_UI_COMMUNITY_B2C_AUTHORITY', env.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY, communityEnv.VITE_APP_UI_COMMUNITY_B2C_AUTHORITY); + const staffHostname = requiredHostnameFromSources( 'VITE_APP_UI_STAFF_BASE_URL', + env.VITE_APP_UI_STAFF_BASE_URL, + staffEnv.VITE_APP_UI_STAFF_BASE_URL, + env.VITE_APP_UI_STAFF_AAD_REDIRECT_URI, + staffEnv.VITE_APP_UI_STAFF_AAD_REDIRECT_URI, ); return { diff --git a/packages/ocom/local-dev-config/vitest.config.ts b/packages/ocom/local-dev-config/vitest.config.ts index 2338cbf45..44d181500 100644 --- a/packages/ocom/local-dev-config/vitest.config.ts +++ b/packages/ocom/local-dev-config/vitest.config.ts @@ -16,10 +16,8 @@ export default mergeConfig(nodeConfig, { { find: '@ocom/local-dev-config/urls', replacement: join(dirname, 'src/urls/index.ts') }, { find: '@ocom/local-dev-config', replacement: join(dirname, 'src/index.ts') }, { find: '@cellix/local-dev/files', replacement: join(dirname, '../../cellix/local-dev/src/files/index.ts') }, - { find: '@cellix/local-dev/process', replacement: join(dirname, '../../cellix/local-dev/src/process/index.ts') }, { find: '@cellix/local-dev/runners', replacement: join(dirname, '../../cellix/local-dev/src/runners/index.ts') }, { find: '@cellix/local-dev/urls', replacement: join(dirname, '../../cellix/local-dev/src/urls/index.ts') }, - { find: '@cellix/local-dev/vite', replacement: join(dirname, '../../cellix/local-dev/src/vite/index.ts') }, { find: '@cellix/local-dev/workspace', replacement: join(dirname, '../../cellix/local-dev/src/workspace/index.ts') }, { find: '@cellix/local-dev/worktree', replacement: join(dirname, '../../cellix/local-dev/src/worktree/index.ts') }, { find: '@cellix/local-dev', replacement: join(dirname, '../../cellix/local-dev/src/index.ts') }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e16c4f37..6521e08fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,17 +64,14 @@ catalogs: specifier: 7.0.0-dev.20260428.1 version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': - specifier: 4.1.6 - version: 4.1.6 + specifier: 4.1.8 + version: 4.1.8 antd: specifier: 6.3.5 version: 6.3.5 archunit: specifier: ^2.1.63 version: 2.1.63 - esbuild: - specifier: 0.27.4 - version: 0.27.4 graphql: specifier: ^16.10.0 version: 16.12.0 @@ -97,8 +94,8 @@ catalogs: specifier: ^19.1.1 version: 19.2.0 react-router-dom: - specifier: 7.15.0 - version: 7.15.0 + specifier: 7.15.1 + version: 7.15.1 rimraf: specifier: 6.0.1 version: 6.0.1 @@ -112,26 +109,27 @@ catalogs: specifier: ^0.28.0 version: 0.28.0 vitest: - specifier: 4.1.6 - version: 4.1.6 + specifier: 4.1.8 + version: 4.1.8 overrides: '@grpc/grpc-js': ^1.14.4 - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 axios: 1.16.0 follow-redirects: ^1.16.0 - vite: 8.0.5 + vite: 8.0.16 jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 brace-expansion@1.1.12: 1.1.13 + brace-expansion@2.0.2: 2.0.3 brace-expansion@5.0.4: 5.0.6 brace-expansion@5.0.5: 5.0.6 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - protobufjs@7.5.4: 7.5.8 + protobufjs@7.5.4: 7.6.3 serve-handler>minimatch: 3.1.5 serialize-javascript@6.0.2: 7.0.5 serialize-javascript@7.0.4: 7.0.5 @@ -151,18 +149,26 @@ overrides: express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': ^3.4.0 azurite>uuid: ^3.4.0 - ws@8.20.0: 8.20.1 + ws@8.20.0: 8.21.0 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.5.8 + protobufjs: 7.6.3 ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 shell-quote@<1.8.4: 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 - ws: 8.20.1 + ws: 8.21.0 shell-quote: 1.8.4 + esbuild: 0.28.1 + form-data@2.5.5: 2.5.6 + form-data@4.0.5: 4.0.6 + immutable: '>=3.8.3' + joi@17.13.3: 17.13.4 + js-yaml@4.1.1: 4.2.0 + launch-editor@2.12.0: 2.14.1 + smol-toml@1.5.2: 1.6.1 packageExtensionsChecksum: sha256-mDviJarBPcwNNCTUf3T37btBxDGgV1wZ/iUGQfx5OCA= @@ -175,7 +181,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.6) + version: 6.3.0(vitest@4.1.8) '@ant-design/cli': specifier: ^6.3.5 version: 6.3.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -220,7 +226,7 @@ importers: version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) chrome-devtools-mcp: specifier: ^0.21.0 version: 0.21.0 @@ -246,14 +252,14 @@ importers: specifier: ^1.0.2 version: 1.0.4(typescript@6.0.3) turbo: - specifier: 2.9.9 - version: 2.9.9 + specifier: 2.9.14 + version: 2.9.14 typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/api: dependencies: @@ -323,7 +329,7 @@ importers: version: link:../../packages/ocom/local-dev-config '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) azurite: specifier: ^3.35.0 version: 3.35.0(@azure/core-client@1.10.1)(@types/node@24.10.1) @@ -338,19 +344,19 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/docs: dependencies: '@docusaurus/core': specifier: 3.10.1 - version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/plugin-content-docs': specifier: ^3.10.1 - version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/preset-classic': specifier: 3.10.1 - version: 3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3) + version: 3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3) '@mdx-js/react': specifier: ^3.0.0 version: 3.1.1(@types/react@19.2.7)(react@19.2.0) @@ -378,13 +384,13 @@ importers: version: link:../../packages/cellix/local-dev '@docusaurus/module-type-aliases': specifier: 3.10.1 - version: 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@docusaurus/tsconfig': specifier: 3.10.1 version: 3.10.1 '@docusaurus/types': specifier: 3.10.1 - version: 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.9.1 @@ -399,10 +405,10 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -411,7 +417,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/server-mongodb-memory-mock: dependencies: @@ -467,7 +473,7 @@ importers: version: link:../../packages/cellix/local-dev '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -476,13 +482,13 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-community: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -512,7 +518,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -527,7 +533,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -546,19 +552,19 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-onboarding': specifier: 'catalog:' version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@testing-library/jest-dom': specifier: ^6.6.4 version: 6.9.1 @@ -570,25 +576,25 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) esbuild: - specifier: 'catalog:' - version: 0.27.4 + specifier: 0.28.1 + version: 0.28.1 jsdom: specifier: ^26.1.0 version: 26.1.0 rollup-plugin-visualizer: specifier: ^6.0.5 - version: 6.0.5(rolldown@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) + version: 6.0.5(rolldown@1.0.3) storybook: specifier: 'catalog:' version: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -596,20 +602,20 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-node-polyfills: specifier: 'catalog:' - version: 0.28.0(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.28.0(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-staff: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../packages/cellix/ui-core @@ -648,7 +654,7 @@ importers: version: 6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) apollo-link-rest: specifier: ^0.9.0 - version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) + version: 0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2) less: specifier: ^4.4.0 version: 4.4.2 @@ -663,7 +669,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -682,19 +688,19 @@ importers: version: 19.2.3(@types/react@19.2.7) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) esbuild: - specifier: 'catalog:' - version: 0.27.4 + specifier: 0.28.1 + version: 0.28.1 jsdom: specifier: ^26.1.0 version: 26.1.0 rollup-plugin-visualizer: specifier: ^6.0.5 - version: 6.0.5(rolldown@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) + version: 6.0.5(rolldown@1.0.3) tailwindcss: specifier: ^3.4.17 version: 3.4.18(tsx@4.21.0)(yaml@2.8.3) @@ -702,14 +708,14 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-node-polyfills: specifier: 'catalog:' - version: 0.28.0(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.28.0(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/api-services-spec: devDependencies: @@ -745,7 +751,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-rolldown: devDependencies: @@ -757,7 +763,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -769,7 +775,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-typescript: {} @@ -780,16 +786,16 @@ importers: version: link:../config-typescript '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@vitest/browser-playwright': - specifier: 4.1.6 - version: 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/domain-seedwork: devDependencies: @@ -801,7 +807,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -810,7 +816,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/event-bus-seedwork-node: dependencies: @@ -829,7 +835,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -860,7 +866,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.6) + version: 6.3.0(vitest@4.1.8) '@cellix/config-typescript': specifier: workspace:* version: link:../config-typescript @@ -869,7 +875,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -878,7 +884,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/graphql-core: dependencies: @@ -897,7 +903,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -906,7 +912,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/local-dev: devDependencies: @@ -921,7 +927,7 @@ importers: version: 22.19.15 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -930,7 +936,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/mongoose-seedwork: dependencies: @@ -949,7 +955,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) mongodb: specifier: 'catalog:' version: 6.18.0 @@ -967,7 +973,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/serenity-framework: dependencies: @@ -1022,7 +1028,7 @@ importers: version: 19.2.7 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) playwright: specifier: 1.59.0 version: 1.59.0 @@ -1037,7 +1043,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/server-mongodb-memory-mock-seedwork: dependencies: @@ -1084,7 +1090,7 @@ importers: version: 5.0.5 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1093,7 +1099,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/ui-core: dependencies: @@ -1121,19 +1127,19 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-onboarding': specifier: 'catalog:' version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@testing-library/react': specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -1144,14 +1150,14 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: 4.1.6 - version: 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/browser-playwright': - specifier: 4.1.6 - version: 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -1160,7 +1166,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1172,7 +1178,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1260,7 +1266,7 @@ importers: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/serenity-framework': specifier: workspace:* version: link:../../cellix/serenity-framework @@ -1348,7 +1354,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/e2e-tests: dependencies: @@ -1447,7 +1453,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1456,7 +1462,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/context-spec: dependencies: @@ -1509,7 +1515,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/domain: dependencies: @@ -1561,7 +1567,7 @@ importers: version: 3.42.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1570,7 +1576,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/event-handler: dependencies: @@ -1623,7 +1629,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1632,7 +1638,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/graphql-handler: dependencies: @@ -1666,7 +1672,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1675,7 +1681,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/local-dev-config: dependencies: @@ -1694,7 +1700,7 @@ importers: version: 22.19.15 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1703,7 +1709,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/persistence: dependencies: @@ -1743,7 +1749,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1752,7 +1758,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/rest: dependencies: @@ -1805,7 +1811,7 @@ importers: version: 1.1.6 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1814,7 +1820,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-blob-storage: dependencies: @@ -1855,7 +1861,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1913,7 +1919,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1922,7 +1928,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-token-validation: dependencies: @@ -1941,7 +1947,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1950,7 +1956,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-accounts: dependencies: @@ -1962,7 +1968,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -1992,7 +1998,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -2014,16 +2020,16 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@types/react': specifier: ^19.1.11 version: 19.2.7 @@ -2040,11 +2046,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-admin: dependencies: @@ -2056,7 +2062,7 @@ importers: version: 7.22.7(antd@6.3.5(luxon@3.7.2)(moment@2.30.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2086,7 +2092,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -2108,16 +2114,16 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: ^10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@types/react': specifier: ^19.1.11 version: 19.2.7 @@ -2134,11 +2140,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-root: dependencies: @@ -2178,16 +2184,16 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@types/react': specifier: ^19.1.11 version: 19.2.7 @@ -2204,17 +2210,17 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-shared: dependencies: '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2238,7 +2244,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@ant-design/icons': specifier: 'catalog:' @@ -2257,16 +2263,16 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@types/react': specifier: ^19.1.11 version: 19.2.7 @@ -2283,11 +2289,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-shared: dependencies: @@ -2296,7 +2302,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2320,7 +2326,7 @@ importers: version: 3.3.0(oidc-client-ts@3.4.1)(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/archunit-tests': specifier: workspace:* @@ -2342,16 +2348,16 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-docs': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) '@storybook/react-vite': specifier: 'catalog:' - version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@types/react': specifier: ^19.1.11 version: 19.2.7 @@ -2359,11 +2365,11 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: 4.1.6 - version: 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -2375,16 +2381,16 @@ importers: version: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook-addon-apollo-client: specifier: ^9.0.0 - version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) + version: 9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0) typescript: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-community-management: dependencies: @@ -2402,7 +2408,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2423,11 +2429,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-finance: dependencies: @@ -2445,7 +2451,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2466,11 +2472,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-root: dependencies: @@ -2509,11 +2515,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-tech-admin: dependencies: @@ -2531,7 +2537,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2552,11 +2558,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-user-management: dependencies: @@ -2577,7 +2583,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2598,11 +2604,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-shared: dependencies: @@ -2611,7 +2617,7 @@ importers: version: 6.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@apollo/client': specifier: ^3.13.9 - version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@cellix/ui-core': specifier: workspace:* version: link:../../cellix/ui-core @@ -2632,7 +2638,7 @@ importers: version: 19.2.0(react@19.2.0) react-router-dom: specifier: 'catalog:' - version: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -2659,11 +2665,11 @@ importers: specifier: 'catalog:' version: 6.0.3 vite: - specifier: 8.0.5 - version: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + specifier: 8.0.16 + version: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -4371,158 +4377,158 @@ packages: resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} engines: {node: '>=18.0.0'} - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -4860,7 +4866,7 @@ packages: resolution: {integrity: sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==} peerDependencies: typescript: '>= 4.3.x' - vite: 8.0.5 + vite: 8.0.16 peerDependenciesMeta: typescript: optional: true @@ -5508,12 +5514,12 @@ packages: '@oxc-project/types@0.121.0': resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} - '@oxc-project/types@0.122.0': - resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} - '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-resolver/binding-android-arm-eabi@11.14.0': resolution: {integrity: sha512-jB47iZ/thvhE+USCLv+XY3IknBbkKr/p7OBsQDTHode/GPw+OHRlit3NQ1bjt1Mj8V2CS7iHdSDYobZ1/0gagQ==} cpu: [arm] @@ -5884,15 +5890,24 @@ packages: '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + '@protobufjs/fetch@1.1.0': resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} '@protobufjs/inquire@1.1.1': resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -6194,8 +6209,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -6206,8 +6221,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -6218,8 +6233,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.12': - resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -6230,8 +6245,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': - resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -6242,8 +6257,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': - resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -6255,8 +6270,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -6269,22 +6284,22 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -6297,8 +6312,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': - resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -6311,8 +6326,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': - resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -6324,8 +6339,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': - resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -6335,9 +6350,9 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': - resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.55': @@ -6346,8 +6361,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -6358,8 +6373,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': - resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -6367,12 +6382,12 @@ packages: '@rolldown/pluginutils@1.0.0-beta.55': resolution: {integrity: sha512-vajw/B3qoi7aYnnD4BQ4VoCcXQWnF0roSwE2iynbNxgW4l9mFwtLmLmUhpDdcTBfKyZm1p/T0D13qG94XBLohA==} - '@rolldown/pluginutils@1.0.0-rc.12': - resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} - '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-inject@5.0.5': resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} @@ -6514,8 +6529,8 @@ packages: '@storybook/addon-vitest@10.4.2': resolution: {integrity: sha512-gq1ZVr0IOLgiS2PzaJqHxKLczwrXvA5g3W2k/FVJA19fJNFNUut1IyQCQRSJTXLLXbnNaa8I0wjGGc2BoLarJg==} peerDependencies: - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 '@vitest/runner': ^3.0.0 || ^4.0.0 storybook: ^10.4.2 vitest: ^3.0.0 || ^4.0.0 @@ -6533,15 +6548,15 @@ packages: resolution: {integrity: sha512-d3+i9vbbUfV6hvT90qabmy1WmC4bEJ7iAYDm0217doeA+S6awF25GF0qOy9gN9waU4NMntHoVpdB1YQO2wUj/w==} peerDependencies: storybook: ^10.4.2 - vite: 8.0.5 + vite: 8.0.16 '@storybook/csf-plugin@10.4.2': resolution: {integrity: sha512-GqX/2DeF3/jKs5D7gpDiuT9gd0c/f2TKcnQ5av4/s3YqeN+0nhm7btkCrDfgF16uzE1Zj3OrkxvB3AOkfxWgDg==} peerDependencies: - esbuild: '*' + esbuild: 0.28.1 rollup: ^4.59.0 storybook: ^10.4.2 - vite: 8.0.5 + vite: 8.0.16 webpack: ^5.105.4 peerDependenciesMeta: esbuild: @@ -6589,7 +6604,7 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 storybook: ^10.4.2 - vite: 8.0.5 + vite: 8.0.16 '@storybook/react@10.4.2': resolution: {integrity: sha512-NfEH3CrdCAgUV4Z7SPN3Iw6nofcueqtRj8iHuo77GNjz0qSfuVi9iS7a8o7x7QFSeIBZwS0Jv3CgmhN8qvoLjg==} @@ -6744,33 +6759,33 @@ packages: '@ts-morph/common@0.28.1': resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} - '@turbo/darwin-64@2.9.9': - resolution: {integrity: sha512-hTEiNu2ABZZOO1qbjnKASI8eF3BdOOzU6iKv5w5uGOK65DDMc10cS40N1kqM99YT0uSAGUwNu6GdFctRPeEeVA==} + '@turbo/darwin-64@2.9.14': + resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.9': - resolution: {integrity: sha512-MinO40EEcP5mJiTVpfjtEulsEBhVeryfq21QhYtJZ8hQJLHGgy459rcmDVAY8/JERe4dkVU4KW+zoLF22o01EA==} + '@turbo/darwin-arm64@2.9.14': + resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.9': - resolution: {integrity: sha512-7JNLw88Isk+gMlbsC8pulLDkrqe2B827ZsKFEHilb17AC6Xn/62pzH7afjY7fEU6Ayp4XP/vGhlRWOzqBvBvIQ==} + '@turbo/linux-64@2.9.14': + resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.9': - resolution: {integrity: sha512-0pnXDwPw1rHii98JZPRg7SvsjIzy7jrhkwGU9Jy5fVYoMdYd3P2vbtLfII+OJ0Mm4Ar5yykdHDTz3RWiRI1o9g==} + '@turbo/linux-arm64@2.9.14': + resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.9': - resolution: {integrity: sha512-vjDQycz4gQVvIq4n2rPtiiIESwJlAc406qtkiZlqyL+fHZEd9SxYNlBIFYtc5cuMuwrk+sIKrhN7XvwjmvS9YQ==} + '@turbo/windows-64@2.9.14': + resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.9': - resolution: {integrity: sha512-V6NiH43oCctepbOdQFp7UjqLyK8p6Tt824QA+G4TE+B1BBHu80A0W8OCL+H7uBJ3XZjAj/hvPDw3k3l65DoDGw==} + '@turbo/windows-arm64@2.9.14': + resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} cpu: [arm64] os: [win32] @@ -7071,40 +7086,40 @@ packages: peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 babel-plugin-react-compiler: ^1.0.0 - vite: 8.0.5 + vite: 8.0.16 peerDependenciesMeta: '@rolldown/plugin-babel': optional: true babel-plugin-react-compiler: optional: true - '@vitest/browser-playwright@4.1.6': - resolution: {integrity: sha512-4csoeyl/qwHyxU2zNL0++WaoDr8YJDXOQPwWPNJoTZ+QzcdO3INYKgF5Zfz730Io7zbkuv914aZmfQ+QE+1Hvw==} + '@vitest/browser-playwright@4.1.8': + resolution: {integrity: sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==} peerDependencies: playwright: 1.59.0 - vitest: 4.1.6 + vitest: 4.1.8 - '@vitest/browser@4.1.6': - resolution: {integrity: sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==} + '@vitest/browser@4.1.8': + resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==} peerDependencies: - vitest: 4.1.6 + vitest: 4.1.8 - '@vitest/coverage-istanbul@4.1.6': - resolution: {integrity: sha512-lOt/VDh+sihAx3OUxCE5CC0qZfAhIzE3Dxw75NJ3P0C6ruUgT9b/jZKECE1ctpbxSVic9OkLdXz5UEX39ks4Sw==} + '@vitest/coverage-istanbul@4.1.8': + resolution: {integrity: sha512-/h514nMZMKI6foh21mVgO1zlCH6pdDamwKMbla1uLU2GMxTlfp0PQMxovWozmzQdCIQYZ2XXEzg2zNZom2zAOg==} peerDependencies: - vitest: 4.1.6 + vitest: 4.1.8 '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.1.6': - resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.1.6': - resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 - vite: 8.0.5 + vite: 8.0.16 peerDependenciesMeta: msw: optional: true @@ -7114,26 +7129,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.6': - resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/runner@4.1.6': - resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/snapshot@4.1.6': - resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.1.6': - resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.6': - resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -7631,8 +7646,8 @@ packages: brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} @@ -8700,8 +8715,8 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -8985,12 +9000,12 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + form-data@2.5.6: + resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} engines: {node: '>= 0.12'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} format@0.2.2: @@ -9212,7 +9227,7 @@ packages: crossws: ~0.3 graphql: ^15.10.1 || ^16 uWebSockets.js: ^20 - ws: 8.20.1 + ws: 8.21.0 peerDependenciesMeta: '@fastify/websocket': optional: true @@ -9297,6 +9312,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} @@ -9498,9 +9517,8 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - immutable@3.7.6: - resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} - engines: {node: '>=0.8.0'} + immutable@5.1.6: + resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -9865,7 +9883,7 @@ packages: isomorphic-ws@5.0.0: resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: - ws: 8.20.1 + ws: 8.21.0 istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} @@ -9905,8 +9923,8 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + joi@17.13.4: + resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -9921,8 +9939,8 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true jsbi@4.3.2: @@ -10025,8 +10043,8 @@ packages: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} - launch-editor@2.12.0: - resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} less@4.4.2: resolution: {integrity: sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==} @@ -11725,8 +11743,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - protobufjs@7.5.8: - resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} + protobufjs@7.6.3: + resolution: {integrity: sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -11877,8 +11895,8 @@ packages: peerDependencies: react: '>=15' - react-router-dom@7.15.0: - resolution: {integrity: sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==} + react-router-dom@7.15.1: + resolution: {integrity: sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -11889,8 +11907,8 @@ packages: peerDependencies: react: '>=15' - react-router@7.15.0: - resolution: {integrity: sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==} + react-router@7.15.1: + resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -12163,8 +12181,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-rc.12: - resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -12496,8 +12514,8 @@ packages: resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} engines: {node: '>=8.0.0'} - smol-toml@1.5.2: - resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} snake-case@3.0.4: @@ -12918,6 +12936,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -13063,8 +13085,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.9: - resolution: {integrity: sha512-3xfzXE/yTjhh0S5dIWlE+3E+J9A09REpLI1ZqVh2+HrNZoVzZn0pkvjiRgVK/Ev3PF9XnaTwCntTx+CADWXcyA==} + turbo@2.9.14: + resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} hasBin: true type-fest@0.13.1: @@ -13344,16 +13366,16 @@ packages: vite-plugin-node-polyfills@0.28.0: resolution: {integrity: sha512-NXct/ci2ef4fRyCfTb8fk2HmR80Rv7icLd+cRH41TnUugDzdKMFKqFPpZYCFUInZMMem9bkLv5pkq02+7Xu7+w==} peerDependencies: - vite: 8.0.5 + vite: 8.0.16 - vite@8.0.5: - resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 + '@vitejs/devtools': ^0.1.18 + esbuild: 0.28.1 jiti: 2.6.1 less: ^4.0.0 sass: ^1.70.0 @@ -13389,23 +13411,23 @@ packages: yaml: optional: true - vitest@4.1.6: - resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.6 - '@vitest/browser-preview': 4.1.6 - '@vitest/browser-webdriverio': 4.1.6 - '@vitest/coverage-istanbul': 4.1.6 - '@vitest/coverage-v8': 4.1.6 - '@vitest/ui': 4.1.6 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' - vite: 8.0.5 + vite: 8.0.16 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -13634,8 +13656,8 @@ packages: write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13885,13 +13907,13 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.6)': + '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.8)': dependencies: callsites: 4.2.0 minimist: 1.2.8 parsecurrency: 1.1.1 ts-morph: 27.0.2 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@ant-design/cli@6.3.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: @@ -14029,7 +14051,7 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -14046,7 +14068,7 @@ snapshots: tslib: 2.8.1 zen-observable-ts: 1.2.5 optionalDependencies: - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.21.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -14163,7 +14185,7 @@ snapshots: chalk: 4.1.2 fb-watchman: 2.0.2 graphql: 16.12.0 - immutable: 3.7.6 + immutable: 5.1.6 invariant: 2.2.4 nullthrows: 1.1.1 relay-runtime: 12.0.0 @@ -14376,7 +14398,7 @@ snapshots: dependencies: '@azure/core-auth': 1.10.1 axios: 1.16.0 - form-data: 2.5.5 + form-data: 2.5.6 tough-cookie: 2.5.0 tslib: 1.14.1 tunnel: 0.0.6 @@ -15742,7 +15764,7 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/babel@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/babel@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -15754,7 +15776,7 @@ snapshots: '@babel/runtime': 7.28.4 '@babel/traverse': 7.29.0 '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.2 tslib: 2.8.1 @@ -15767,32 +15789,32 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/bundler@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: '@babel/core': 7.29.0 - '@docusaurus/babel': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/babel': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@docusaurus/cssnano-preset': 3.10.1 '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.105.4(esbuild@0.27.4)) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.105.4(esbuild@0.28.1)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.105.4(esbuild@0.27.4)) - css-loader: 6.11.0(webpack@5.105.4(esbuild@0.27.4)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)) + copy-webpack-plugin: 11.0.0(webpack@5.105.4(esbuild@0.28.1)) + css-loader: 6.11.0(webpack@5.105.4(esbuild@0.28.1)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.105.4(esbuild@0.28.1)) cssnano: 6.1.2(postcss@8.5.10) - file-loader: 6.2.0(webpack@5.105.4(esbuild@0.27.4)) + file-loader: 6.2.0(webpack@5.105.4(esbuild@0.28.1)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.105.4(esbuild@0.27.4)) - null-loader: 4.0.1(webpack@5.105.4(esbuild@0.27.4)) + mini-css-extract-plugin: 2.9.4(webpack@5.105.4(esbuild@0.28.1)) + null-loader: 4.0.1(webpack@5.105.4(esbuild@0.28.1)) postcss: 8.5.10 - postcss-loader: 7.3.4(postcss@8.5.10)(typescript@6.0.3)(webpack@5.105.4(esbuild@0.27.4)) + postcss-loader: 7.3.4(postcss@8.5.10)(typescript@6.0.3)(webpack@5.105.4(esbuild@0.28.1)) postcss-preset-env: 10.4.0(postcss@8.5.10) - terser-webpack-plugin: 5.4.0(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)) + terser-webpack-plugin: 5.4.0(esbuild@0.28.1)(webpack@5.105.4(esbuild@0.28.1)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.27.4)))(webpack@5.105.4(esbuild@0.27.4)) - webpack: 5.105.4(esbuild@0.27.4) - webpackbar: 7.0.0(webpack@5.105.4(esbuild@0.27.4)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.28.1)))(webpack@5.105.4(esbuild@0.28.1)) + webpack: 5.105.4(esbuild@0.28.1) + webpackbar: 7.0.0(webpack@5.105.4(esbuild@0.28.1)) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -15808,15 +15830,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/babel': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/bundler': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/babel': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/bundler': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) boxen: 6.2.1 chalk: 4.1.2 @@ -15832,7 +15854,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.2 html-tags: 3.3.1 - html-webpack-plugin: 5.6.5(webpack@5.105.4(esbuild@0.27.4)) + html-webpack-plugin: 5.6.5(webpack@5.105.4(esbuild@0.28.1)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 @@ -15842,7 +15864,7 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.105.4(esbuild@0.27.4)) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.105.4(esbuild@0.28.1)) react-router: 5.3.4(react@19.2.0) react-router-config: 5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0) react-router-dom: 5.3.4(react@19.2.0) @@ -15851,9 +15873,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.4(webpack@5.105.4(esbuild@0.27.4)) + webpack-dev-server: 5.2.4(webpack@5.105.4(esbuild@0.28.1)) webpack-merge: 6.0.1 transitivePeerDependencies: - '@parcel/css' @@ -15883,16 +15905,16 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/mdx-loader@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mdx-js/mdx': 3.1.1 '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.105.4(esbuild@0.27.4)) + file-loader: 6.2.0(webpack@5.105.4(esbuild@0.28.1)) fs-extra: 11.3.2 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -15908,9 +15930,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.27.4)))(webpack@5.105.4(esbuild@0.27.4)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.28.1)))(webpack@5.105.4(esbuild@0.28.1)) vfile: 6.0.3 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -15918,9 +15940,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/module-type-aliases@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/history': 4.7.11 '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 @@ -15936,17 +15958,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-content-blog@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 @@ -15959,7 +15981,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -15978,28 +16000,28 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.2 - js-yaml: 4.1.1 + js-yaml: 4.2.0 lodash: 4.18.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -16018,18 +16040,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-content-pages@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) tslib: 2.8.1 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -16048,12 +16070,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -16075,11 +16097,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-debug@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -16103,11 +16125,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-google-analytics@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) tslib: 2.8.1 @@ -16129,11 +16151,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-google-gtag@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/gtag.js': 0.0.20 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -16156,11 +16178,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-google-tag-manager@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) tslib: 2.8.1 @@ -16182,14 +16204,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-sitemap@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -16213,18 +16235,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/plugin-svgr@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@svgr/core': 8.1.0(typescript@6.0.3) '@svgr/webpack': 8.1.0(typescript@6.0.3) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) tslib: 2.8.1 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -16243,23 +16265,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3)': - dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-debug': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-google-analytics': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-google-gtag': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-google-tag-manager': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-sitemap': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-svgr': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/theme-classic': 3.10.1(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3) - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3)': + dependencies: + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-debug': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-google-analytics': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-google-gtag': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-google-tag-manager': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-sitemap': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-svgr': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/theme-classic': 3.10.1(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: @@ -16288,21 +16310,21 @@ snapshots: '@types/react': 19.2.7 react: 19.2.0 - '@docusaurus/theme-classic@3.10.1(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': + '@docusaurus/theme-classic@3.10.1(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3)': dependencies: - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/plugin-content-blog': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/plugin-content-pages': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 @@ -16336,13 +16358,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/theme-common@3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@docusaurus/mdx-loader': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/mdx-loader': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/module-type-aliases': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@types/history': 4.7.11 '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 @@ -16360,17 +16382,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3)': + '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.45.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3)(typescript@6.0.3)': dependencies: '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.45.0)(algoliasearch@5.45.0)(search-insights@2.17.3) '@docsearch/react': 4.3.2(@algolia/client-search@5.45.0)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(search-insights@2.17.3) - '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) - '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/plugin-content-docs': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3) + '@docusaurus/theme-common': 3.10.1(@docusaurus/plugin-content-docs@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.0))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@6.0.3))(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@docusaurus/theme-translations': 3.10.1 - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-validation': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-validation': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) algoliasearch: 5.45.0 algoliasearch-helper: 3.26.1(algoliasearch@5.45.0) clsx: 2.1.1 @@ -16409,19 +16431,19 @@ snapshots: '@docusaurus/tsconfig@3.10.1': {} - '@docusaurus/types@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/types@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 '@types/mdast': 4.0.4 '@types/react': 19.2.7 commander: 5.1.0 - joi: 17.13.3 + joi: 17.13.4 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)' utility-types: 3.11.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -16430,9 +16452,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/utils-common@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -16443,14 +16465,14 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/utils-validation@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/utils': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) fs-extra: 11.3.2 - joi: 17.13.3 - js-yaml: 4.1.1 + joi: 17.13.4 + js-yaml: 4.2.0 lodash: 4.18.1 tslib: 2.8.1 transitivePeerDependencies: @@ -16462,29 +16484,29 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@docusaurus/utils@3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@docusaurus/logger': 3.10.1 - '@docusaurus/types': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@docusaurus/utils-common': 3.10.1(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/types': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@docusaurus/utils-common': 3.10.1(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.105.4(esbuild@0.27.4)) + file-loader: 6.2.0(webpack@5.105.4(esbuild@0.28.1)) fs-extra: 11.3.2 github-slugger: 1.5.0 globby: 11.1.0 gray-matter: 4.0.3 jiti: 2.6.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 lodash: 4.18.1 micromatch: 4.0.8 p-queue: 6.6.2 prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.27.4)))(webpack@5.105.4(esbuild@0.27.4)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.28.1)))(webpack@5.105.4(esbuild@0.28.1)) utility-types: 3.11.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - '@swc/core' - esbuild @@ -16547,82 +16569,82 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 - '@esbuild/aix-ppc64@0.27.4': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.4': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.4': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.4': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.4': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.4': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.4': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.4': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.4': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.4': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.4': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.4': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.4': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.4': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.4': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.4': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.4': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.4': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.4': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.4': + '@esbuild/win32-x64@0.28.1': optional: true '@fastify/busboy@3.2.0': {} @@ -16896,10 +16918,10 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@whatwg-node/disposablestack': 0.0.6 graphql: 16.12.0 - graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.20.1) - isomorphic-ws: 5.0.0(ws@8.20.1) + graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.21.0) + isomorphic-ws: 5.0.0(ws@8.21.0) tslib: 2.8.1 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@fastify/websocket' - bufferutil @@ -16927,9 +16949,9 @@ snapshots: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@types/ws': 8.18.1 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.21.0) tslib: 2.8.1 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -17051,7 +17073,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 jose: 5.10.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 lodash: 4.18.1 scuid: 1.1.0 tslib: 2.8.1 @@ -17101,10 +17123,10 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - isomorphic-ws: 5.0.0(ws@8.20.1) + isomorphic-ws: 5.0.0(ws@8.21.0) sync-fetch: 0.6.0-2 tslib: 2.8.1 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - '@fastify/websocket' - '@types/node' @@ -17148,7 +17170,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.8 + protobufjs: 7.6.3 yargs: 17.7.2 '@hapi/hoek@9.3.0': {} @@ -17196,11 +17218,11 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 6.0.3 @@ -17601,7 +17623,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - protobufjs: 7.5.8 + protobufjs: 7.6.3 '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': dependencies: @@ -17839,10 +17861,10 @@ snapshots: '@oxc-project/types@0.121.0': {} - '@oxc-project/types@0.122.0': {} - '@oxc-project/types@0.127.0': {} + '@oxc-project/types@0.133.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.14.0': optional: true @@ -18153,15 +18175,23 @@ snapshots: '@protobufjs/eventemitter@1.1.0': {} + '@protobufjs/eventemitter@1.1.1': {} + '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/inquire': 1.1.1 + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/float@1.0.2': {} '@protobufjs/inquire@1.1.1': {} + '@protobufjs/inquire@1.1.2': {} + '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} @@ -18521,67 +18551,67 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.55': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.12': + '@rolldown/binding-android-arm64@1.0.3': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.55': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.55': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.12': + '@rolldown/binding-darwin-x64@1.0.3': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.55': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.55': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.55': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.55': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.55': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.55': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.55': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.55(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -18592,32 +18622,31 @@ snapshots: - '@emnapi/runtime' optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.55': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-beta.55': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true '@rolldown/pluginutils@1.0.0-beta.55': {} - '@rolldown/pluginutils@1.0.0-rc.12': {} - '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-inject@5.0.5': dependencies: '@rollup/pluginutils': 5.3.0 @@ -18790,10 +18819,10 @@ snapshots: axe-core: 4.11.0 storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/addon-docs@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4))': + '@storybook/addon-docs@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.0) - '@storybook/csf-plugin': 10.4.2(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + '@storybook/csf-plugin': 10.4.2(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/icons': 2.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@storybook/react-dom-shim': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) react: 19.2.0 @@ -18813,39 +18842,39 @@ snapshots: dependencies: storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/addon-vitest@10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6)': + '@storybook/addon-vitest@10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/runner': 4.1.6 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/runner': 4.1.8 + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom - '@storybook/builder-vite@10.4.2(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4))': + '@storybook/builder-vite@10.4.2(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1))': dependencies: - '@storybook/csf-plugin': 10.4.2(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + '@storybook/csf-plugin': 10.4.2(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ts-dedent: 2.2.0 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.4.2(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4))': + '@storybook/csf-plugin@10.4.2(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1))': dependencies: storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) unplugin: 2.3.11 optionalDependencies: - esbuild: 0.27.4 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - webpack: 5.105.4(esbuild@0.27.4) + esbuild: 0.28.1 + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + webpack: 5.105.4(esbuild@0.28.1) '@storybook/global@5.0.0': {} @@ -18869,11 +18898,11 @@ snapshots: react-dom: 19.2.0(react@19.2.0) storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/react-vite@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4))': + '@storybook/react-vite@10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.28.1)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 10.4.2(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) + '@storybook/builder-vite': 10.4.2(esbuild@0.28.1)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.28.1)) '@storybook/react': 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) empathic: 2.0.1 magic-string: 0.30.21 @@ -18883,7 +18912,7 @@ snapshots: resolve: 1.22.11 storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tsconfig-paths: 4.2.0 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -19068,22 +19097,22 @@ snapshots: path-browserify: 1.0.1 tinyglobby: 0.2.15 - '@turbo/darwin-64@2.9.9': + '@turbo/darwin-64@2.9.14': optional: true - '@turbo/darwin-arm64@2.9.9': + '@turbo/darwin-arm64@2.9.14': optional: true - '@turbo/linux-64@2.9.9': + '@turbo/linux-64@2.9.14': optional: true - '@turbo/linux-arm64@2.9.9': + '@turbo/linux-arm64@2.9.14': optional: true - '@turbo/windows-64@2.9.9': + '@turbo/windows-64@2.9.14': optional: true - '@turbo/windows-arm64@2.9.9': + '@turbo/windows-arm64@2.9.14': optional: true '@tybys/wasm-util@0.10.1': @@ -19395,18 +19424,18 @@ snapshots: '@vercel/oidc@3.0.5': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser-playwright@4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: - '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -19414,30 +19443,30 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser-playwright@4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: - '@vitest/browser': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser@4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.6 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.21.0 transitivePeerDependencies: - bufferutil - msw @@ -19445,24 +19474,24 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.6 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + ws: 8.21.0 transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/coverage-istanbul@4.1.6(vitest@4.1.6)': + '@vitest/coverage-istanbul@4.1.8(vitest@4.1.8)': dependencies: '@babel/core': 7.29.0 '@istanbuljs/schema': 0.1.3 @@ -19474,7 +19503,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19486,48 +19515,48 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.1.6': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.6': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.6': + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 4.1.6 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.1.6': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 @@ -19535,7 +19564,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.1.6': {} + '@vitest/spy@4.1.8': {} '@vitest/utils@3.2.4': dependencies: @@ -19543,9 +19572,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.6': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.6 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -19862,9 +19891,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 4.0.4 - apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): + apollo-link-rest@0.9.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(qs@6.15.2): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 qs: 6.15.2 @@ -20018,7 +20047,7 @@ snapshots: axios@1.16.0: dependencies: follow-redirects: 1.16.0(debug@4.4.3) - form-data: 4.0.5 + form-data: 4.0.6 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -20064,12 +20093,12 @@ snapshots: b4a@1.7.3: {} - babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.105.4(esbuild@0.27.4)): + babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@babel/core': 7.29.0 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -20205,7 +20234,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.2: + brace-expansion@2.0.3: dependencies: balanced-match: 1.0.2 @@ -20736,7 +20765,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.105.4(esbuild@0.27.4)): + copy-webpack-plugin@11.0.0(webpack@5.105.4(esbuild@0.28.1)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -20744,7 +20773,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 7.0.5 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) core-js-compat@3.47.0: dependencies: @@ -20762,7 +20791,7 @@ snapshots: cosmiconfig@8.3.6(typescript@6.0.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: @@ -20843,7 +20872,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.105.4(esbuild@0.27.4)): + css-loader@6.11.0(webpack@5.105.4(esbuild@0.28.1)): dependencies: icss-utils: 5.1.0(postcss@8.5.10) postcss: 8.5.10 @@ -20854,9 +20883,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.10) @@ -20864,10 +20893,10 @@ snapshots: postcss: 8.5.10 schema-utils: 4.3.3 serialize-javascript: 7.0.5 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) optionalDependencies: clean-css: 5.3.3 - esbuild: 0.27.4 + esbuild: 0.28.1 css-prefers-color-scheme@10.0.0(postcss@8.5.10): dependencies: @@ -21379,34 +21408,34 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild@0.27.4: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -21636,11 +21665,11 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - file-loader@6.2.0(webpack@5.105.4(esbuild@0.27.4)): + file-loader@6.2.0(webpack@5.105.4(esbuild@0.28.1)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) filename-reserved-regex@2.0.0: {} @@ -21729,21 +21758,21 @@ snapshots: form-data-encoder@2.1.4: {} - form-data@2.5.5: + form-data@2.5.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 safe-buffer: 5.2.1 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 format@0.2.2: {} @@ -22005,11 +22034,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.20.1 + ws: 8.21.0 graphql@14.7.0: dependencies: @@ -22037,7 +22066,7 @@ snapshots: '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -22089,6 +22118,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -22252,7 +22285,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.5(webpack@5.105.4(esbuild@0.27.4)): + html-webpack-plugin@5.6.5(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -22260,7 +22293,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.0 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) htmlparser2@6.1.0: dependencies: @@ -22385,7 +22418,7 @@ snapshots: immediate@3.0.6: {} - immutable@3.7.6: {} + immutable@5.1.6: {} import-fresh@3.3.1: dependencies: @@ -22710,9 +22743,9 @@ snapshots: isomorphic-timers-promises@1.0.1: {} - isomorphic-ws@5.0.0(ws@8.20.1): + isomorphic-ws@5.0.0(ws@8.21.0): dependencies: - ws: 8.20.1 + ws: 8.21.0 istanbul-lib-coverage@3.2.2: {} @@ -22763,7 +22796,7 @@ snapshots: jiti@2.6.1: {} - joi@17.13.3: + joi@17.13.4: dependencies: '@hapi/hoek': 9.3.0 '@hapi/topo': 5.1.0 @@ -22782,7 +22815,7 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.1: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -22808,7 +22841,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.1 + ws: 8.21.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -22894,12 +22927,12 @@ snapshots: fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimist: 1.2.8 oxc-resolver: 11.14.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) picocolors: 1.1.1 picomatch: 4.0.4 - smol-toml: 1.5.2 + smol-toml: 1.6.1 strip-json-comments: 5.0.3 typescript: 6.0.3 zod: 4.1.13 @@ -22917,7 +22950,7 @@ snapshots: dependencies: package-json: 8.1.1 - launch-editor@2.12.0: + launch-editor@2.14.1: dependencies: picocolors: 1.1.1 shell-quote: 1.8.4 @@ -23725,11 +23758,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.105.4(esbuild@0.27.4)): + mini-css-extract-plugin@2.9.4(webpack@5.105.4(esbuild@0.28.1)): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) minimalistic-assert@1.0.1: {} @@ -23745,7 +23778,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 2.0.3 minimist@1.2.8: {} @@ -24021,11 +24054,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.105.4(esbuild@0.27.4)): + null-loader@4.0.1(webpack@5.105.4(esbuild@0.28.1)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) nullthrows@1.1.1: {} @@ -24646,13 +24679,13 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - postcss-loader@7.3.4(postcss@8.5.10)(typescript@6.0.3)(webpack@5.105.4(esbuild@0.27.4)): + postcss-loader@7.3.4(postcss@8.5.10)(typescript@6.0.3)(webpack@5.105.4(esbuild@0.28.1)): dependencies: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 2.6.1 postcss: 8.5.10 semver: 7.7.4 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - typescript @@ -24998,15 +25031,15 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.5.8: + protobufjs@7.6.3: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 + '@protobufjs/inquire': 1.1.2 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 @@ -25148,11 +25181,11 @@ snapshots: dependencies: react: 19.2.0 - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.105.4(esbuild@0.27.4)): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@babel/runtime': 7.28.4 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)' - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) react-oidc-context@3.3.0(oidc-client-ts@3.4.1)(react@19.2.0): dependencies: @@ -25176,11 +25209,11 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router-dom@7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router-dom@7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - react-router: 7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-router@5.3.4(react@19.2.0): dependencies: @@ -25195,7 +25228,7 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router@7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router@7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.1.1 react: 19.2.0 @@ -25577,38 +25610,35 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - rolldown@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.12 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 - '@rolldown/binding-darwin-x64': 1.0.0-rc.12 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - rollup-plugin-visualizer@6.0.5(rolldown@1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)): + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + rollup-plugin-visualizer@6.0.5(rolldown@1.0.3): dependencies: open: 8.4.2 picomatch: 4.0.4 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: - rolldown: 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + rolldown: 1.0.3 rrweb-cssom@0.8.0: {} @@ -25944,7 +25974,7 @@ snapshots: slugify@1.6.6: {} - smol-toml@1.5.2: {} + smol-toml@1.6.1: {} snake-case@3.0.4: dependencies: @@ -26061,9 +26091,9 @@ snapshots: stoppable@1.1.0: {} - storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): + storybook-addon-apollo-client@9.0.0(@apollo/client@3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(graphql@16.12.0)(react@19.2.0): dependencies: - '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.20.1))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@apollo/client': 3.14.0(@types/react@19.2.7)(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.21.0))(graphql@16.12.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) graphql: 16.12.0 react: 19.2.0 @@ -26076,14 +26106,14 @@ snapshots: '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 '@webcontainer/env': 1.1.1 - esbuild: 0.27.4 + esbuild: 0.28.1 open: 10.2.0 oxc-parser: 0.127.0 oxc-resolver: 11.20.0 recast: 0.23.11 semver: 7.7.4 use-sync-external-store: 1.6.0(react@19.2.0) - ws: 8.20.1 + ws: 8.21.0 optionalDependencies: '@types/react': 19.2.7 transitivePeerDependencies: @@ -26339,15 +26369,15 @@ snapshots: - '@azure/core-client' - supports-color - terser-webpack-plugin@5.4.0(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)): + terser-webpack-plugin@5.4.0(esbuild@0.28.1)(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.44.1 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) optionalDependencies: - esbuild: 0.27.4 + esbuild: 0.28.1 terser@5.44.1: dependencies: @@ -26424,6 +26454,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -26529,7 +26564,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.4 + esbuild: 0.28.1 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -26542,14 +26577,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.9.9: + turbo@2.9.14: optionalDependencies: - '@turbo/darwin-64': 2.9.9 - '@turbo/darwin-arm64': 2.9.9 - '@turbo/linux-64': 2.9.9 - '@turbo/linux-arm64': 2.9.9 - '@turbo/windows-64': 2.9.9 - '@turbo/windows-arm64': 2.9.9 + '@turbo/darwin-64': 2.9.14 + '@turbo/darwin-arm64': 2.9.14 + '@turbo/linux-64': 2.9.14 + '@turbo/linux-arm64': 2.9.14 + '@turbo/windows-64': 2.9.14 + '@turbo/windows-arm64': 2.9.14 type-fest@0.13.1: {} @@ -26744,14 +26779,14 @@ snapshots: uri-templates@0.2.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.27.4)))(webpack@5.105.4(esbuild@0.27.4)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.105.4(esbuild@0.28.1)))(webpack@5.105.4(esbuild@0.28.1)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) optionalDependencies: - file-loader: 6.2.0(webpack@5.105.4(esbuild@0.27.4)) + file-loader: 6.2.0(webpack@5.105.4(esbuild@0.28.1)) url@0.11.4: dependencies: @@ -26828,63 +26863,57 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-node-polyfills@0.28.0(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vite-plugin-node-polyfills@0.28.0(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@rollup/plugin-inject': 5.0.5 node-stdlib-browser: 1.3.1 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - rollup - vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.10 - rolldown: 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - tinyglobby: 0.2.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.15 - esbuild: 0.27.4 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.2 terser: 5.44.1 tsx: 4.21.0 yaml: 2.8.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.10 - rolldown: 1.0.0-rc.12(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - tinyglobby: 0.2.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.10.1 - esbuild: 0.27.4 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.2 terser: 5.44.1 tsx: 4.21.0 yaml: 2.8.3 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -26896,27 +26925,27 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.15 - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/coverage-istanbul': 4.1.8(vitest@4.1.8) happy-dom: 20.9.0 jsdom: 26.1.0 transitivePeerDependencies: - msw - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.9.0)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -26928,13 +26957,13 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.28.1)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/coverage-istanbul': 4.1.8(vitest@4.1.8) happy-dom: 20.9.0 jsdom: 26.1.0 transitivePeerDependencies: @@ -26986,12 +27015,12 @@ snapshots: opener: 1.5.2 picocolors: 1.1.1 sirv: 2.0.4 - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.105.4(esbuild@0.27.4)): + webpack-dev-middleware@7.4.5(webpack@5.105.4(esbuild@0.28.1)): dependencies: colorette: 2.0.20 memfs: 4.51.1 @@ -27000,9 +27029,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) - webpack-dev-server@5.2.4(webpack@5.105.4(esbuild@0.27.4)): + webpack-dev-server@5.2.4(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -27022,7 +27051,7 @@ snapshots: graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.3.0 - launch-editor: 2.12.0 + launch-editor: 2.14.1 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 @@ -27030,10 +27059,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.27.4)) - ws: 8.20.1 + webpack-dev-middleware: 7.4.5(webpack@5.105.4(esbuild@0.28.1)) + ws: 8.21.0 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - bufferutil - debug @@ -27056,7 +27085,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.105.4(esbuild@0.27.4): + webpack@5.105.4(esbuild@0.28.1): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -27080,7 +27109,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.4.0(esbuild@0.27.4)(webpack@5.105.4(esbuild@0.27.4)) + terser-webpack-plugin: 5.4.0(esbuild@0.28.1)(webpack@5.105.4(esbuild@0.28.1)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -27088,14 +27117,14 @@ snapshots: - esbuild - uglify-js - webpackbar@7.0.0(webpack@5.105.4(esbuild@0.27.4)): + webpackbar@7.0.0(webpack@5.105.4(esbuild@0.28.1)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - webpack: 5.105.4(esbuild@0.27.4) + webpack: 5.105.4(esbuild@0.28.1) websocket-driver@0.7.4: dependencies: @@ -27238,7 +27267,7 @@ snapshots: signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - ws@8.20.1: {} + ws@8.21.0: {} wsl-utils@0.1.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ffb5f1b04..f8ac3a5d7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,16 +19,16 @@ catalog: '@serenity-js/cucumber': 3.42.2 '@serenity-js/serenity-bdd': 3.42.2 '@types/node': ^22.19.5 - '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-istanbul': 4.1.8 antd: 6.3.5 archunit: ^2.1.63 - esbuild: 0.27.4 + esbuild: 0.28.1 graphql: ^16.10.0 jsdom: ^26.1.0 mongodb: 6.18.0 mongodb-memory-server: ^10.1.4 mongoose: 8.17.0 - react-router-dom: 7.15.0 + react-router-dom: 7.15.1 react: ^19.1.1 react-dom: ^19.1.1 '@ant-design/icons': ^6.0.2 @@ -42,10 +42,10 @@ catalog: tsx: ^4.21.0 typescript: 6.0.3 "@typescript/native-preview": 7.0.0-dev.20260428.1 - vite: 8.0.5 - vitest: 4.1.6 - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + vite: 8.0.16 + vitest: 4.1.8 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 vite-plugin-node-polyfills: ^0.28.0 auditConfig: @@ -55,6 +55,24 @@ auditConfig: - GHSA-8v8x-cx79-35w7 - GHSA-wpg9-53fq-2r8h - GHSA-q7rr-3cgh-j5r3 + # uuid: override to uuid 14+ breaks azurite (see .snyk SNYK-JS-UUID-16133035) + - GHSA-w5hq-g745-h8pq + # @azure/identity: 3.x→4.x major bump required; transitive via azurite>tedious + - GHSA-m5vv-6r4h-3vj9 + # @opentelemetry/core: 2.x upgrade blocked; see .snyk SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050 + - GHSA-8988-4f7v-96qf + # @ai-sdk/provider-utils: transitive via docusaurus, not exploitable (see .snyk) + - GHSA-866g-f22w-33x8 + # xml2js@0.4.23: 0.4→0.5 major bump would break azurite>@azure/ms-rest-js + - GHSA-776f-qx25-q3cc + # tough-cookie@2.5.x: 2.x→4.x major bump required; transitive via azurite>@azure/ms-rest-js + - GHSA-72xf-g2v4-qvf3 + # js-yaml@3.x: 3→4 major bump required for old transitive consumers; 4.1.1 patched via override + - GHSA-h67p-54hq-rp68 + # @babel/core: 7.x→8.0.0 major bump; no 7.29.1 release exists; low severity, dev tooling only + - GHSA-4x5r-pxfx-6jf8 + # elliptic: 6.6.2 does not exist, 6.6.1 is latest; low severity, dev tooling only + - GHSA-848j-6mx2-7j84 allowBuilds: '@apollo/protobufjs': true @@ -68,8 +86,8 @@ allowBuilds: overrides: '@grpc/grpc-js': ^1.14.4 - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 axios: 1.16.0 follow-redirects: ^1.16.0 vite: "catalog:" @@ -77,12 +95,13 @@ overrides: rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 'brace-expansion@1.1.12': 1.1.13 + 'brace-expansion@2.0.2': 2.0.3 'brace-expansion@5.0.4': 5.0.6 'brace-expansion@5.0.5': 5.0.6 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 - 'protobufjs@7.5.4': 7.5.8 + 'protobufjs@7.5.4': 7.6.3 'serve-handler>minimatch': 3.1.5 'serialize-javascript@6.0.2': 7.0.5 'serialize-javascript@7.0.4': 7.0.5 @@ -102,18 +121,26 @@ overrides: express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': '^3.4.0' 'azurite>uuid': '^3.4.0' - 'ws@8.20.0': 8.20.1 + 'ws@8.20.0': 8.21.0 playwright-core: 1.59.0 playwright: 1.59.0 postcss: 8.5.10 - protobufjs: 7.5.8 + protobufjs: 7.6.3 ip-address: ^10.1.1 fast-uri: ^3.1.2 '@babel/plugin-transform-modules-systemjs': 7.29.4 'shell-quote@<1.8.4': 1.8.4 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 - ws: 8.20.1 + ws: 8.21.0 shell-quote: 1.8.4 + esbuild: 0.28.1 + 'form-data@2.5.5': 2.5.6 + 'form-data@4.0.5': 4.0.6 + immutable: '>=3.8.3' + 'joi@17.13.3': 17.13.4 + 'js-yaml@4.1.1': 4.2.0 + 'launch-editor@2.12.0': 2.14.1 + 'smol-toml@1.5.2': 1.6.1 packageExtensions: '@azure/functions-opentelemetry-instrumentation@0.1.0': From ea2b6a2a62c5b6a27217cad6250471020c83258b Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 22 Jun 2026 09:31:03 -0400 Subject: [PATCH 29/40] pipeline fix --- pnpm-lock.yaml | 430 +++++++++++++++++++++++--------------------- pnpm-workspace.yaml | 15 +- 2 files changed, 236 insertions(+), 209 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5439108f6..cc1bd2771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,8 +64,8 @@ catalogs: specifier: 7.0.0-dev.20260428.1 version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': - specifier: 4.1.6 - version: 4.1.6 + specifier: 4.1.8 + version: 4.1.8 antd: specifier: 6.3.5 version: 6.3.5 @@ -115,12 +115,12 @@ catalogs: specifier: ^0.28.0 version: 0.28.0 vitest: - specifier: 4.1.6 - version: 4.1.6 + specifier: 4.1.8 + version: 4.1.8 overrides: - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 happy-dom: 20.10.1 axios: 1.16.0 follow-redirects: ^1.16.0 @@ -173,7 +173,7 @@ overrides: webpack-dev-server>http-proxy-middleware: 3.0.7 joi: ^17.13.4 launch-editor: ^2.14.1 - react-router: 7.15.1 + react-router-dom@7.15.0>react-router: 7.15.1 packageExtensionsChecksum: sha256-mDviJarBPcwNNCTUf3T37btBxDGgV1wZ/iUGQfx5OCA= @@ -186,7 +186,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.6) + version: 6.3.0(vitest@4.1.8) '@ant-design/cli': specifier: ^6.3.5 version: 6.3.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -231,7 +231,7 @@ importers: version: 7.0.0-dev.20260428.1 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) chrome-devtools-mcp: specifier: ^0.21.0 version: 0.21.0 @@ -264,7 +264,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/api: dependencies: @@ -328,7 +328,7 @@ importers: version: link:../../packages/cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) archunit: specifier: 'catalog:' version: 2.1.63 @@ -346,7 +346,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/docs: dependencies: @@ -407,7 +407,7 @@ importers: version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -416,7 +416,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/server-mongodb-memory-mock: dependencies: @@ -466,7 +466,7 @@ importers: version: link:../../packages/cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -478,7 +478,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-community: dependencies: @@ -548,7 +548,7 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -569,7 +569,7 @@ importers: version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) esbuild: specifier: 'catalog:' version: 0.27.4 @@ -599,7 +599,7 @@ importers: version: 0.28.0(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) apps/ui-staff: dependencies: @@ -675,7 +675,7 @@ importers: version: 6.0.1(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) esbuild: specifier: 'catalog:' version: 0.27.4 @@ -699,7 +699,7 @@ importers: version: 0.28.0(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/api-services-spec: devDependencies: @@ -735,7 +735,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-rolldown: devDependencies: @@ -747,7 +747,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -759,7 +759,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/config-typescript: {} @@ -770,16 +770,16 @@ importers: version: link:../config-typescript '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@vitest/browser-playwright': - specifier: 4.1.6 - version: 4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) typescript: specifier: 'catalog:' version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/domain-seedwork: devDependencies: @@ -791,7 +791,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -800,7 +800,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/event-bus-seedwork-node: dependencies: @@ -819,7 +819,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -850,7 +850,7 @@ importers: devDependencies: '@amiceli/vitest-cucumber': specifier: ^6.3.0 - version: 6.3.0(vitest@4.1.6) + version: 6.3.0(vitest@4.1.8) '@cellix/config-typescript': specifier: workspace:* version: link:../config-typescript @@ -859,7 +859,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -868,7 +868,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/graphql-core: dependencies: @@ -887,7 +887,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -896,7 +896,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/mongoose-seedwork: dependencies: @@ -915,7 +915,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) mongodb: specifier: 'catalog:' version: 6.18.0 @@ -933,7 +933,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/serenity-framework: dependencies: @@ -988,7 +988,7 @@ importers: version: 19.2.7 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) playwright: specifier: 1.59.0 version: 1.59.0 @@ -1003,7 +1003,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/server-mongodb-memory-mock-seedwork: dependencies: @@ -1050,7 +1050,7 @@ importers: version: 5.0.5 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1059,7 +1059,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/service-blob-storage: dependencies: @@ -1081,7 +1081,7 @@ importers: version: link:../config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1090,7 +1090,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cellix/ui-core: dependencies: @@ -1124,7 +1124,7 @@ importers: version: 10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -1141,14 +1141,14 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: 4.1.6 - version: 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/browser-playwright': - specifier: 4.1.6 - version: 4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -1169,7 +1169,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/acceptance-api: dependencies: @@ -1351,7 +1351,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom-verification/e2e-tests: dependencies: @@ -1453,7 +1453,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1462,7 +1462,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/context-spec: dependencies: @@ -1518,7 +1518,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/domain: dependencies: @@ -1570,7 +1570,7 @@ importers: version: 3.42.2 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1579,7 +1579,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/event-handler: dependencies: @@ -1632,7 +1632,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1641,7 +1641,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/graphql-handler: dependencies: @@ -1675,7 +1675,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1684,7 +1684,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/persistence: dependencies: @@ -1724,7 +1724,7 @@ importers: version: link:../../ocom-verification/archunit-tests '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1733,7 +1733,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/rest: dependencies: @@ -1786,7 +1786,7 @@ importers: version: 1.1.6 '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1795,7 +1795,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-blob-storage: dependencies: @@ -1811,7 +1811,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1820,7 +1820,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-mongoose: dependencies: @@ -1842,7 +1842,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1900,7 +1900,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1909,7 +1909,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/service-token-validation: dependencies: @@ -1928,7 +1928,7 @@ importers: version: link:../../cellix/config-vitest '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) rimraf: specifier: 'catalog:' version: 6.0.1 @@ -1937,7 +1937,7 @@ importers: version: 6.0.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-accounts: dependencies: @@ -2004,7 +2004,7 @@ importers: version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -2031,7 +2031,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-admin: dependencies: @@ -2098,7 +2098,7 @@ importers: version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: ^10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -2125,7 +2125,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-route-root: dependencies: @@ -2168,7 +2168,7 @@ importers: version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -2195,7 +2195,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-community-shared: dependencies: @@ -2247,7 +2247,7 @@ importers: version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -2274,7 +2274,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-shared: dependencies: @@ -2332,7 +2332,7 @@ importers: version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(esbuild@0.27.4)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(webpack@5.105.4(esbuild@0.27.4)) '@storybook/addon-vitest': specifier: 'catalog:' - version: 10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6) + version: 10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8) '@storybook/react': specifier: 10.4.2 version: 10.4.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(typescript@6.0.3) @@ -2346,11 +2346,11 @@ importers: specifier: ^19.1.6 version: 19.2.3(@types/react@19.2.7) '@vitest/browser': - specifier: 4.1.6 - version: 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) + specifier: 4.1.8 + version: 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) '@vitest/coverage-istanbul': specifier: 'catalog:' - version: 4.1.6(vitest@4.1.6) + version: 4.1.8(vitest@4.1.8) jsdom: specifier: 'catalog:' version: 26.1.0 @@ -2371,7 +2371,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-community-management: dependencies: @@ -2414,7 +2414,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-finance: dependencies: @@ -2457,7 +2457,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-root: dependencies: @@ -2500,7 +2500,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-tech-admin: dependencies: @@ -2543,7 +2543,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-route-user-management: dependencies: @@ -2589,7 +2589,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages/ocom/ui-staff-shared: dependencies: @@ -2650,7 +2650,7 @@ importers: version: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -6600,8 +6600,8 @@ packages: '@storybook/addon-vitest@10.4.2': resolution: {integrity: sha512-gq1ZVr0IOLgiS2PzaJqHxKLczwrXvA5g3W2k/FVJA19fJNFNUut1IyQCQRSJTXLLXbnNaa8I0wjGGc2BoLarJg==} peerDependencies: - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 '@vitest/runner': ^3.0.0 || ^4.0.0 storybook: ^10.4.2 vitest: ^3.0.0 || ^4.0.0 @@ -7164,30 +7164,30 @@ packages: babel-plugin-react-compiler: optional: true - '@vitest/browser-playwright@4.1.6': - resolution: {integrity: sha512-4csoeyl/qwHyxU2zNL0++WaoDr8YJDXOQPwWPNJoTZ+QzcdO3INYKgF5Zfz730Io7zbkuv914aZmfQ+QE+1Hvw==} + '@vitest/browser-playwright@4.1.8': + resolution: {integrity: sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==} peerDependencies: playwright: 1.59.0 - vitest: 4.1.6 + vitest: 4.1.8 - '@vitest/browser@4.1.6': - resolution: {integrity: sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==} + '@vitest/browser@4.1.8': + resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==} peerDependencies: - vitest: 4.1.6 + vitest: 4.1.8 - '@vitest/coverage-istanbul@4.1.6': - resolution: {integrity: sha512-lOt/VDh+sihAx3OUxCE5CC0qZfAhIzE3Dxw75NJ3P0C6ruUgT9b/jZKECE1ctpbxSVic9OkLdXz5UEX39ks4Sw==} + '@vitest/coverage-istanbul@4.1.8': + resolution: {integrity: sha512-/h514nMZMKI6foh21mVgO1zlCH6pdDamwKMbla1uLU2GMxTlfp0PQMxovWozmzQdCIQYZ2XXEzg2zNZom2zAOg==} peerDependencies: - vitest: 4.1.6 + vitest: 4.1.8 '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/expect@4.1.6': - resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.1.6': - resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: 8.0.16 @@ -7200,26 +7200,26 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.6': - resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/runner@4.1.6': - resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/snapshot@4.1.6': - resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@4.1.6': - resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.6': - resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -9924,6 +9924,9 @@ packages: resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} engines: {node: '>=12'} + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -11239,6 +11242,9 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} @@ -11964,7 +11970,7 @@ packages: resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} peerDependencies: react: '>=15' - react-router: 7.15.1 + react-router: '>=5' react-router-dom@5.3.4: resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} @@ -11978,6 +11984,11 @@ packages: react: '>=18' react-dom: '>=18' + react-router@5.3.4: + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' + react-router@7.15.1: resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==} engines: {node: '>=20.0.0'} @@ -13481,20 +13492,20 @@ packages: yaml: optional: true - vitest@4.1.6: - resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.6 - '@vitest/browser-preview': 4.1.6 - '@vitest/browser-webdriverio': 4.1.6 - '@vitest/coverage-istanbul': 4.1.6 - '@vitest/coverage-v8': 4.1.6 - '@vitest/ui': 4.1.6 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: 20.10.1 jsdom: '*' vite: 8.0.16 @@ -13981,13 +13992,13 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.6)': + '@amiceli/vitest-cucumber@6.3.0(vitest@4.1.8)': dependencies: callsites: 4.2.0 minimist: 1.2.8 parsecurrency: 1.1.1 ts-morph: 27.0.2 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) '@ant-design/cli@6.3.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: @@ -16087,9 +16098,9 @@ snapshots: react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.0)' react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.0))(webpack@5.105.4(esbuild@0.27.4)) - react-router: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - react-router-config: 5.1.1(react-router@7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) - react-router-dom: 5.3.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router: 5.3.4(react@19.2.0) + react-router-config: 5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0) + react-router-dom: 5.3.4(react@19.2.0) semver: 7.7.4 serve-handler: 6.1.7 tinypool: 1.1.1 @@ -16546,7 +16557,7 @@ snapshots: prismjs: 1.30.0 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - react-router-dom: 5.3.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router-dom: 5.3.4(react@19.2.0) rtlcss: 4.3.0 tslib: 2.8.1 utility-types: 3.11.0 @@ -19045,16 +19056,16 @@ snapshots: dependencies: storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@storybook/addon-vitest@10.4.2(@vitest/browser-playwright@4.1.6)(@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6))(@vitest/runner@4.1.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.6)': + '@storybook/addon-vitest@10.4.2(@vitest/browser-playwright@4.1.8)(@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8))(@vitest/runner@4.1.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vitest@4.1.8)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) storybook: 10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) optionalDependencies: - '@vitest/browser': 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/runner': 4.1.6 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/runner': 4.1.8 + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - react - react-dom @@ -19298,7 +19309,7 @@ snapshots: dependencies: minimatch: 10.2.4 path-browserify: 1.0.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 '@turbo/darwin-64@2.9.9': optional: true @@ -19632,13 +19643,13 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser-playwright@4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: - '@vitest/browser': 4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw @@ -19646,29 +19657,29 @@ snapshots: - vite optional: true - '@vitest/browser-playwright@4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser-playwright@4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: - '@vitest/browser': 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/browser': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) playwright: 1.59.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser@4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.6 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -19677,16 +19688,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6)': + '@vitest/browser@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.6 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -19694,7 +19705,7 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-istanbul@4.1.6(vitest@4.1.6)': + '@vitest/coverage-istanbul@4.1.8(vitest@4.1.8)': dependencies: '@babel/core': 7.29.7 '@istanbuljs/schema': 0.1.3 @@ -19706,7 +19717,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -19718,26 +19729,26 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/expect@4.1.6': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -19747,19 +19758,19 @@ snapshots: dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.6': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.6': + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 4.1.6 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.1.6': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 @@ -19767,7 +19778,7 @@ snapshots: dependencies: tinyspy: 4.0.4 - '@vitest/spy@4.1.6': {} + '@vitest/spy@4.1.8': {} '@vitest/utils@3.2.4': dependencies: @@ -19775,9 +19786,9 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.6': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.6 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -22931,6 +22942,8 @@ snapshots: is-yarn-global@0.4.1: {} + isarray@0.0.1: {} + isarray@1.0.0: {} isarray@2.0.5: {} @@ -23379,8 +23392,8 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-dir@2.1.0: @@ -24642,6 +24655,10 @@ snapshots: path-to-regexp@0.1.13: {} + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + path-to-regexp@3.3.0: {} path-to-regexp@8.4.0: {} @@ -25403,24 +25420,22 @@ snapshots: oidc-client-ts: 3.4.1 react: 19.2.0 - react-router-config@5.1.1(react-router@7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): + react-router-config@5.1.1(react-router@5.3.4(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 react: 19.2.0 - react-router: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router: 5.3.4(react@19.2.0) - react-router-dom@5.3.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + react-router-dom@5.3.4(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 react: 19.2.0 - react-router: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router: 5.3.4(react@19.2.0) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - transitivePeerDependencies: - - react-dom react-router-dom@7.15.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: @@ -25428,6 +25443,19 @@ snapshots: react-dom: 19.2.0(react@19.2.0) react-router: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router@5.3.4(react@19.2.0): + dependencies: + '@babel/runtime': 7.28.4 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 19.2.0 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + react-router@7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: cookie: 1.1.1 @@ -27100,15 +27128,15 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@22.19.15)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -27118,29 +27146,29 @@ snapshots: std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 vite: 8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 22.19.15 - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/coverage-istanbul': 4.1.8(vitest@4.1.8) happy-dom: 20.10.1 jsdom: 26.1.0 transitivePeerDependencies: - msw - vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@24.10.1)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-istanbul@4.1.8)(happy-dom@20.10.1)(jsdom@26.1.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -27150,15 +27178,15 @@ snapshots: std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 vite: 8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/node': 24.10.1 - '@vitest/browser-playwright': 4.1.6(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.6) - '@vitest/coverage-istanbul': 4.1.6(vitest@4.1.6) + '@vitest/browser-playwright': 4.1.8(playwright@1.59.0)(vite@8.0.16(@types/node@24.10.1)(esbuild@0.27.4)(jiti@2.6.1)(less@4.4.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.8) + '@vitest/coverage-istanbul': 4.1.8(vitest@4.1.8) happy-dom: 20.10.1 jsdom: 26.1.0 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4823339e1..2b53a69ec 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,7 +20,7 @@ catalog: '@serenity-js/cucumber': 3.42.2 '@serenity-js/serenity-bdd': 3.42.2 '@types/node': ^22.19.5 - '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-istanbul': 4.1.8 antd: 6.3.5 archunit: ^2.1.63 esbuild: 0.27.4 @@ -44,9 +44,9 @@ catalog: typescript: 6.0.3 "@typescript/native-preview": 7.0.0-dev.20260428.1 vite: 8.0.16 - vitest: 4.1.6 - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + vitest: 4.1.8 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 vite-plugin-node-polyfills: ^0.28.0 auditConfig: @@ -57,7 +57,6 @@ auditConfig: - GHSA-wpg9-53fq-2r8h - GHSA-q7rr-3cgh-j5r3 - GHSA-869p-cjfg-cm3x # jws@4.0.0: Improperly Verifies HMAC Signature (transitive from azurite) - - GHSA-g8mr-85jm-7xhm # @vitest/browser <=4.1.7: Browser Mode API can proxy CDP; dev/test-only tooling, pinned to 4.1.6 to match catalog, not in production runtime allowBuilds: '@apollo/protobufjs': true @@ -70,8 +69,8 @@ allowBuilds: snyk: true overrides: - '@vitest/browser': 4.1.6 - '@vitest/browser-playwright': 4.1.6 + '@vitest/browser': 4.1.8 + '@vitest/browser-playwright': 4.1.8 happy-dom: 20.10.1 axios: 1.16.0 follow-redirects: ^1.16.0 @@ -124,7 +123,7 @@ overrides: 'webpack-dev-server>http-proxy-middleware': 3.0.7 joi: ^17.13.4 launch-editor: ^2.14.1 - react-router: 7.15.1 + 'react-router-dom@7.15.0>react-router': 7.15.1 packageExtensions: '@azure/functions-opentelemetry-instrumentation@0.1.0': From 4770730ea5a63e2e817e48d1ac0d78c19a78622f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 22 Jun 2026 15:59:54 -0400 Subject: [PATCH 30/40] small readabiliyy improvements --- packages/cellix/serenity-framework/README.md | 9 +- .../serenity-framework/src/servers/index.ts | 2 +- .../mongo-memory-process-test-server.ts | 4 +- .../servers/mongo-memory-test-server.test.ts | 1 - .../src/servers/process-test-server.test.ts | 55 ++++++--- .../src/servers/process-test-server.ts | 113 ++++++++---------- .../src/servers/test-mongo-server.ts | 3 +- .../src/servers/community-ui-portal-server.ts | 11 +- .../src/servers/staff-ui-portal-server.ts | 11 +- .../e2e-tests/src/servers/test-api-server.ts | 18 +-- .../src/servers/test-azurite-server.ts | 22 ++-- .../src/servers/test-mongo-server.ts | 3 +- .../src/servers/test-oauth2-server.ts | 5 +- .../src/shared/environment/dev-script.ts | 6 - .../shared/environment/test-environment.ts | 2 + 15 files changed, 123 insertions(+), 142 deletions(-) delete mode 100644 packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index 3067ae41b..a725d434c 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -43,7 +43,7 @@ const mongo = new MongoMemoryProcessTestServer({ executable: 'pnpm', spawnArgs: ['run', 'dev'], cwd: '/repo/apps/server-mongodb-memory-mock', - connectionString: () => mongoConnectionString, + connectionString: mongoConnectionString, dbName, portsToCloseBeforeStart: () => mongoPort, readyMarker: 'MongoDB Memory Replica Set ready at:', @@ -55,8 +55,7 @@ const api = new ProcessTestServer({ executable: 'pnpm', spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], cwd: '/repo/apps/api', - extraEnv: () => ({ COSMOSDB_CONNECTION_STRING: mongo.getConnectionString() }), - getUrl: () => 'https://api.localhost:1355/api/graphql', + url: 'https://api.localhost:1355/api/graphql', readyMarker: 'Functions:', }); @@ -65,7 +64,7 @@ const community = new ProcessUiTestServer({ executable: 'pnpm', spawnArgs: ['run', 'dev'], cwd: '/repo/apps/ui-community', - getUrl: () => 'https://community.localhost:1355', + url: 'https://community.localhost:1355', readyMarker: 'ready in', }); @@ -92,6 +91,8 @@ const staffContext = await infrastructure.newPortalContext('staff'); Register server objects directly. Dependencies that need one another receive those references through normal object construction; `dependsOn` describes startup order only. Each UI portal owns its browser-context recipe, so `newPortalContext(name)` scopes navigation to that portal without any portal being special-cased. The framework never imports app paths or app-specific environment helpers — pass those into server constructors from the consumer package. +`ProcessTestServer` performs a narrow health check after it sees the ready marker: GraphQL URLs ending in `/graphql` use a small POST `{ __typename }` request, and root web URLs use the fetch default. Service-specific URLs, raw localhost dependencies, and non-HTTP URLs trust the ready marker. + ## API acceptance infrastructure API-only acceptance suites use the smaller infrastructure manager. It composes any consumer-owned `TestServer` implementations without launching a browser, and owns dependency-ordered startup, scenario reset, and shutdown. diff --git a/packages/cellix/serenity-framework/src/servers/index.ts b/packages/cellix/serenity-framework/src/servers/index.ts index c7021b112..bee280d68 100644 --- a/packages/cellix/serenity-framework/src/servers/index.ts +++ b/packages/cellix/serenity-framework/src/servers/index.ts @@ -6,6 +6,6 @@ export type { MongoMemorySeedContext, MongoMemorySeedDataFunction } from './mong export type { MongoMemoryTestServerOptions } from './mongo-memory-test-server.ts'; export { MongoMemoryTestServer } from './mongo-memory-test-server.ts'; export { createSpawnEnvironment } from './process-environment.ts'; -export type { ProcessHealthProbe, ProcessTestServerOptions } from './process-test-server.ts'; +export type { ProcessTestServerOptions } from './process-test-server.ts'; export { ProcessTestServer, ProcessUiTestServer } from './process-test-server.ts'; export type { SeedDataFunction, TestServer, UiTestServer } from './test-server.ts'; diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts index 7aefb36b5..91c275cfa 100644 --- a/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-process-test-server.ts @@ -2,7 +2,7 @@ import { clearMongoMemoryDatabase, type MongoMemorySeedDataFunction } from './mo import { ProcessTestServer, type ProcessTestServerOptions } from './process-test-server.ts'; /** Options used by {@link MongoMemoryProcessTestServer}. */ -export interface MongoMemoryProcessTestServerOptions extends Omit { +export interface MongoMemoryProcessTestServerOptions extends Omit { /** Database name. */ dbName: string; @@ -24,7 +24,7 @@ export class MongoMemoryProcessTestServer extends ProcessTestServer { constructor(private readonly mongoOptions: MongoMemoryProcessTestServerOptions) { super({ ...mongoOptions, - getUrl: () => resolveValue(mongoOptions.connectionString), + url: resolveValue(mongoOptions.connectionString), }); } diff --git a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts index f33e9da6f..141c7fad1 100644 --- a/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts +++ b/packages/cellix/serenity-framework/src/servers/mongo-memory-test-server.test.ts @@ -32,7 +32,6 @@ describe('MongoMemoryProcessTestServer', () => { cwd: process.cwd(), dbName: 'owner-community', executable: process.execPath, - probe: false, readyMarker: 'READY', seedData, serverName: 'mongo process', diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts index 2533e4005..32a4a7489 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts @@ -29,15 +29,14 @@ describe('ProcessTestServer', () => { childProcessMock.execFileSync.mockReset(); }); - it('starts a process and trusts the ready marker when probing is disabled', async () => { + it('starts a process and trusts the ready marker for non-HTTP URLs', async () => { const server = new ProcessTestServer({ serverName: 'marker-only server', executable: process.execPath, spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], cwd: process.cwd(), readyMarker: 'READY', - getUrl: () => 'http://unused.test', - probe: false, + url: 'process://marker-only', shutdownTimeoutMs: 500, }); @@ -57,8 +56,7 @@ describe('ProcessTestServer', () => { spawnArgs: ['-e', "console.log('READY'); setTimeout(() => process.exit(0), 20)"], cwd: process.cwd(), readyMarker: 'READY', - getUrl: () => 'http://unused.test', - probe: false, + url: 'process://short-lived', shutdownTimeoutMs: 500, }); @@ -68,34 +66,63 @@ describe('ProcessTestServer', () => { expect(server.isRunning()).toBe(false); }); - it('closes configured ports before checking whether the server is already running', async () => { + it('closes configured ports before spawning', async () => { childProcessMock.execFileSync.mockReturnValue('123\n456\n'); const kill = vi.spyOn(process, 'kill').mockImplementation(() => true); - const isAlreadyRunning = vi.fn(async () => true); const server = new ProcessTestServer({ serverName: 'fixed-port server', executable: process.execPath, - spawnArgs: ['-e', "console.log('READY')"], + spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], cwd: process.cwd(), readyMarker: 'READY', - getUrl: () => 'http://unused.test', - isAlreadyRunning, + url: 'process://fixed-port', portsToCloseBeforeStart: () => 27_017, - probe: false, + shutdownTimeoutMs: 500, }); - await server.start(); - try { + await server.start(); + expect(childProcessMock.execFileSync).toHaveBeenCalledWith('lsof', ['-ti', 'tcp:27017'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], }); expect(kill).toHaveBeenCalledWith(123, 'SIGTERM'); expect(kill).toHaveBeenCalledWith(456, 'SIGTERM'); - expect(childProcessMock.execFileSync.mock.invocationCallOrder[0] ?? 0).toBeLessThan(isAlreadyRunning.mock.invocationCallOrder[0] ?? 0); + expect(server.isRunning()).toBe(true); } finally { + await server.stop(); kill.mockRestore(); } }); + + it('uses a POST typename probe for GraphQL URLs by default', async () => { + const fetch = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(new Response('', { status: 404 })) + .mockResolvedValueOnce(new Response('', { status: 200 })); + const server = new ProcessTestServer({ + serverName: 'graphql server', + executable: process.execPath, + spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], + cwd: process.cwd(), + readyMarker: 'READY', + url: 'http://127.0.0.1:4000/graphql', + shutdownTimeoutMs: 500, + }); + + try { + await server.start(); + + expect(fetch).toHaveBeenLastCalledWith('http://127.0.0.1:4000/graphql', { + body: JSON.stringify({ query: '{ __typename }' }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + signal: expect.any(AbortSignal) as AbortSignal, + }); + } finally { + await server.stop(); + fetch.mockRestore(); + } + }); }); diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.ts index 272f42a6e..fd06111de 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.ts @@ -2,17 +2,8 @@ import { type ChildProcess, execFileSync, spawn } from 'node:child_process'; import { createSpawnEnvironment } from './process-environment.ts'; import type { TestServer, UiTestServer } from './test-server.ts'; -/** Configuration for health probes used by {@link ProcessTestServer}. */ -export interface ProcessHealthProbe { - /** URL to probe after the ready marker is observed. */ - url: string | (() => string); - - /** Request options supplied to `fetch`. */ - requestInit?: RequestInit | (() => RequestInit); - - /** Predicate that decides whether the probe response is healthy. Defaults to `response.ok`. */ - isHealthy?: (response: Response) => boolean | Promise; -} +const healthCheckTimeoutMs = 3_000; +const healthCheckIntervalMs = 500; /** Options used by {@link ProcessTestServer}. */ export interface ProcessTestServerOptions { @@ -32,13 +23,7 @@ export interface ProcessTestServerOptions { readyMarker: string | RegExp; /** URL exposed by the server. */ - getUrl: () => string; - - /** Additional process environment values. */ - extraEnv?: Record | (() => Record); - - /** Health probe configuration. Defaults to probing `getUrl()`. Use `false` to trust the ready marker. */ - probe?: ProcessHealthProbe | false; + url: string; /** Maximum startup time in milliseconds. */ startupTimeoutMs?: number | (() => number); @@ -46,27 +31,15 @@ export interface ProcessTestServerOptions { /** Maximum graceful shutdown time in milliseconds. */ shutdownTimeoutMs?: number | (() => number); - /** Individual health probe timeout in milliseconds. */ - healthProbeTimeoutMs?: number | (() => number); - - /** Delay between health probes in milliseconds. */ - healthProbeIntervalMs?: number | (() => number); - - /** Return true when the server is already reachable before spawning. */ - isAlreadyRunning?: () => Promise; - /** Ports to clear before spawning, useful for fixed-port local dependencies. */ portsToCloseBeforeStart?: number | number[] | (() => number | number[]); - - /** Treat an early process exit as an existing reusable server. */ - isReusableExit?: (stderrOutput: string) => boolean; } /** * Configurable child-process test server. * - * Consumers pass app-specific commands, paths, URLs, and probes through the - * descriptor. The framework owns lifecycle, readiness, probing, and shutdown. + * Consumers pass app-specific commands, paths, and URLs through the descriptor. + * The framework owns lifecycle, readiness checks, and shutdown. */ export class ProcessTestServer implements TestServer { private process: ChildProcess | null = null; @@ -88,10 +61,6 @@ export class ProcessTestServer implements TestServer { this.closePortsBeforeStart(); - if (await this.isAlreadyRunning()) { - return; - } - const executable = this.value(this.options.executable); const spawnArgs = this.value(this.options.spawnArgs); if (!executable || !spawnArgs) { @@ -100,7 +69,7 @@ export class ProcessTestServer implements TestServer { this.process = spawn(executable, spawnArgs, { cwd: this.options.cwd, - env: createSpawnEnvironment(this.value(this.options.extraEnv) ?? {}), + env: createSpawnEnvironment(), detached: this.useDetachedProcessGroup, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -152,17 +121,7 @@ export class ProcessTestServer implements TestServer { * Return the descriptor URL. */ getUrl(): string { - return this.options.getUrl(); - } - - private async isAlreadyRunning(): Promise { - if (this.options.isAlreadyRunning) { - return await this.options.isAlreadyRunning(); - } - if (this.options.probe === false) { - return false; - } - return await this.isProbeReadyWithin(this.value(this.options.healthProbeTimeoutMs) ?? 3_000); + return this.options.url; } private waitForReady(): Promise { @@ -179,7 +138,6 @@ export class ProcessTestServer implements TestServer { reject(new Error(`${this.options.serverName} did not start within ${startupTimeout}ms`)); }, startupTimeout); - let stderrOutput = ''; let ready = false; const resolveWhenReachable = () => { @@ -206,6 +164,7 @@ export class ProcessTestServer implements TestServer { } }); + let stderrOutput = ''; childProcess.stderr?.on('data', (data: Buffer) => { stderrOutput += data.toString(); }); @@ -227,18 +186,17 @@ export class ProcessTestServer implements TestServer { } clearTimeout(timeout); - if (this.options.isReusableExit?.(stderrOutput)) { - resolve(); - return; - } - reject(new Error(`${this.options.serverName} exited unexpectedly (code: ${code}, signal: ${signal}). stderr: ${stderrOutput.slice(-2000)}`)); }); }); } private async waitForProbeReady(startupDeadline: number, startupTimeout: number): Promise { - const probeInterval = this.value(this.options.healthProbeIntervalMs) ?? 500; + if (!shouldHealthCheck(this.getUrl())) { + return; + } + + const probeInterval = healthCheckIntervalMs; const timeoutError = () => new Error(`${this.options.serverName} did not become healthy within ${startupTimeout}ms`); while (true) { @@ -247,7 +205,7 @@ export class ProcessTestServer implements TestServer { throw timeoutError(); } - if (await this.isProbeReadyWithin(Math.min(this.value(this.options.healthProbeTimeoutMs) ?? 3_000, remainingMs))) { + if (await this.isProbeReadyWithin(Math.min(healthCheckTimeoutMs, remainingMs))) { return; } @@ -261,7 +219,8 @@ export class ProcessTestServer implements TestServer { } private async isProbeReadyWithin(timeoutMs: number): Promise { - if (this.options.probe === false) { + const url = this.getUrl(); + if (!shouldHealthCheck(url)) { return true; } @@ -269,12 +228,11 @@ export class ProcessTestServer implements TestServer { try { const controller = new AbortController(); timeout = setTimeout(() => controller.abort(), timeoutMs); - const probe = this.options.probe; - const response = await fetch(this.value(probe?.url) ?? this.getUrl(), { - ...(this.value(probe?.requestInit) ?? {}), + const response = await fetch(url, { + ...this.defaultRequestInit(url), signal: controller.signal, }); - return probe?.isHealthy ? await probe.isHealthy(response) : response.ok; + return response.ok; } catch { return false; } finally { @@ -289,6 +247,16 @@ export class ProcessTestServer implements TestServer { return typeof marker === 'string' ? text.includes(marker) : marker.test(text); } + private defaultRequestInit(url: string): RequestInit { + return isGraphQLEndpoint(url) + ? { + body: JSON.stringify({ query: '{ __typename }' }), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + } + : {}; + } + private killProcess(childProcess: ChildProcess, signal: NodeJS.Signals): void { if (this.useDetachedProcessGroup && childProcess.pid) { try { @@ -343,6 +311,29 @@ export class ProcessTestServer implements TestServer { } } +function isGraphQLEndpoint(url: string): boolean { + try { + return new URL(url).pathname.endsWith('/graphql'); + } catch { + return false; + } +} + +function shouldHealthCheck(url: string): boolean { + try { + const parsedUrl = new URL(url); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + return false; + } + if (parsedUrl.pathname.endsWith('/graphql')) { + return true; + } + return parsedUrl.pathname === '/' && parsedUrl.hostname !== '127.0.0.1' && parsedUrl.hostname !== 'localhost'; + } catch { + return false; + } +} + /** * Child-process server for browser UI portals. */ diff --git a/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts b/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts index dea415b23..1c62dd328 100644 --- a/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts +++ b/packages/ocom-verification/acceptance-api/src/servers/test-mongo-server.ts @@ -7,12 +7,11 @@ export const mongoDbName = 'owner-community'; const mongoReplSetName = 'globaldb'; export const testMongoServer = new MongoMemoryProcessTestServer({ - connectionString: mongoConnectionString, + connectionString: mongoConnectionString(), cwd: mongodbMemoryMockDir, dbName: mongoDbName, executable: 'pnpm', portsToCloseBeforeStart: getMongoPort, - probe: false, readyMarker: 'MongoDB Memory Replica Set ready at:', seedData: seedDatabase, serverName: 'TestMongoMemoryServer', diff --git a/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts b/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts index 5992358eb..5c492df9b 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/community-ui-portal-server.ts @@ -3,20 +3,13 @@ import { appPaths } from '../shared/environment/app-paths.ts'; import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; const hostnames = getHostnames(); +const communityUrl = buildUrl(hostnames.uiCommunity); export const communityUiPortalServer = new ProcessUiTestServer({ cwd: appPaths.uiCommunityDir, executable: 'pnpm', - extraEnv: () => ({ - BROWSER: 'none', - NODE_ENV: 'development', - }), - getUrl: () => buildUrl(hostnames.uiCommunity), - isAlreadyRunning: () => Promise.resolve(false), - probe: { - url: () => buildUrl(hostnames.uiCommunity), - }, readyMarker: 'ready in', serverName: 'TestCommunityViteServer', spawnArgs: () => ['run', process.env['WORKTREE_NAME'] ? 'dev:worktree' : 'dev'], + url: communityUrl, }); diff --git a/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts b/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts index daeeadcd1..5c3a4afa1 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/staff-ui-portal-server.ts @@ -3,20 +3,13 @@ import { appPaths } from '../shared/environment/app-paths.ts'; import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; const hostnames = getHostnames(); +const staffUrl = buildUrl(hostnames.uiStaff); export const staffUiPortalServer = new ProcessUiTestServer({ cwd: appPaths.uiStaffDir, executable: 'pnpm', - extraEnv: () => ({ - BROWSER: 'none', - NODE_ENV: 'development', - }), - getUrl: () => buildUrl(hostnames.uiStaff), - isAlreadyRunning: () => Promise.resolve(false), - probe: { - url: () => buildUrl(hostnames.uiStaff), - }, readyMarker: 'ready in', serverName: 'TestStaffViteServer', spawnArgs: () => ['run', process.env['WORKTREE_NAME'] ? 'dev:worktree' : 'dev'], + url: staffUrl, }); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts index 6c99be3a3..8ddaf9c2a 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/test-api-server.ts @@ -1,29 +1,15 @@ import { ProcessTestServer } from '@cellix/serenity-framework/servers'; import { appPaths } from '../shared/environment/app-paths.ts'; -import { e2eEnv } from '../shared/environment/dev-script.ts'; import { buildUrl, getHostnames } from '../shared/environment/test-environment.ts'; -import { testMongoServer } from './test-mongo-server.ts'; const hostnames = getHostnames(); +const apiUrl = buildUrl(hostnames.api, '/api/graphql'); export const testApiServer = new ProcessTestServer({ cwd: appPaths.apiDir, executable: 'pnpm', - extraEnv: () => - e2eEnv({ - COSMOSDB_CONNECTION_STRING: testMongoServer.getConnectionString(), - }), - getUrl: () => buildUrl(hostnames.api, '/api/graphql'), - isAlreadyRunning: () => Promise.resolve(false), - probe: { - requestInit: () => ({ - body: JSON.stringify({ query: '{ __typename }' }), - headers: { 'Content-Type': 'application/json' }, - method: 'POST', - }), - url: () => buildUrl(hostnames.api, '/api/graphql'), - }, readyMarker: 'Functions:', serverName: 'TestApiServer', spawnArgs: () => ['run', process.env['WORKTREE_NAME'] ? 'dev:worktree' : 'dev'], + url: apiUrl, }); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts index eed7c4d14..4cabd0718 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/test-azurite-server.ts @@ -1,21 +1,19 @@ -import { join } from 'node:path'; import { ProcessTestServer } from '@cellix/serenity-framework/servers'; import { getAzuritePorts } from '@ocom-verification/verification-shared/environment'; import { appPaths } from '../shared/environment/app-paths.ts'; +const azuriteUrl = `http://127.0.0.1:${getAzuritePorts().blob}`; +const azuritePorts = () => { + const { blob, queue, table } = getAzuritePorts(); + return [blob, queue, table]; +}; + export const testAzuriteServer = new ProcessTestServer({ cwd: appPaths.apiDir, - executable: 'node', - extraEnv: () => { - const binDir = join(appPaths.apiDir, 'node_modules', '.bin'); - const { PATH: pathValue = '' } = process.env; - return { PATH: `${binDir}:${pathValue}` }; - }, - getUrl: () => `http://127.0.0.1:${getAzuritePorts().blob}`, - isReusableExit: (stderrOutput) => stderrOutput.includes('EADDRINUSE'), - portsToCloseBeforeStart: () => getAzuritePorts().blob, - probe: false, + executable: 'pnpm', + portsToCloseBeforeStart: azuritePorts, readyMarker: '[azurite] started', serverName: 'TestAzuriteServer', - spawnArgs: ['start-azurite.mjs'], + spawnArgs: ['run', 'azurite'], + url: azuriteUrl, }); diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts index f4d7df8a7..c967a0b89 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/test-mongo-server.ts @@ -7,12 +7,11 @@ const mongoDbName = 'owner-community'; const mongoReplSetName = 'globaldb'; export const testMongoServer = new MongoMemoryProcessTestServer({ - connectionString: mongoConnectionString, + connectionString: mongoConnectionString(), cwd: appPaths.mongodbMemoryMockDir, dbName: mongoDbName, executable: 'pnpm', portsToCloseBeforeStart: getMongoPort, - probe: false, readyMarker: 'MongoDB Memory Replica Set ready at:', seedData: seedDatabase, serverName: 'TestMongoMemoryServer', diff --git a/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts b/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts index 7da2264a6..9a5b1df35 100644 --- a/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts +++ b/packages/ocom-verification/e2e-tests/src/servers/test-oauth2-server.ts @@ -3,14 +3,13 @@ import { appPaths } from '../shared/environment/app-paths.ts'; import { buildUrl, getHostnames, mockOidcIssuer } from '../shared/environment/test-environment.ts'; const hostnames = getHostnames(); +const oidcIssuerUrl = buildUrl(hostnames.mockAuth, new URL(mockOidcIssuer).pathname); export const testOAuth2Server = new ProcessTestServer({ cwd: appPaths.oauth2MockDir, executable: 'pnpm', - getUrl: () => buildUrl(hostnames.mockAuth, new URL(mockOidcIssuer).pathname), - isAlreadyRunning: () => Promise.resolve(false), - probe: false, readyMarker: 'Registered OIDC config', serverName: 'TestOAuth2Server', spawnArgs: () => ['run', process.env['WORKTREE_NAME'] ? 'dev:worktree' : 'dev'], + url: oidcIssuerUrl, }); diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts deleted file mode 100644 index cc59bff72..000000000 --- a/packages/ocom-verification/e2e-tests/src/shared/environment/dev-script.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function e2eEnv(overrides: Record = {}): Record { - return { - E2E: 'true', - ...overrides, - }; -} diff --git a/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts index cfb488e4c..034f96022 100644 --- a/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts +++ b/packages/ocom-verification/e2e-tests/src/shared/environment/test-environment.ts @@ -60,6 +60,8 @@ export function cleanupTestEnvironment(): void { } function loadE2EEnvDefaults(): void { + process.env['E2E'] ??= 'true'; + const currentDir = dirname(fileURLToPath(import.meta.url)); const workspaceRoot = resolve(currentDir, '../../../../../..'); for (const filePath of [resolve(workspaceRoot, 'apps/ui-community/.env.e2e'), resolve(workspaceRoot, 'apps/ui-staff/.env.e2e')]) { From 9c556be67f823e8459f27014d77115b391fd2168 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 23 Jun 2026 10:05:02 -0400 Subject: [PATCH 31/40] copilot suggestions --- packages/cellix/serenity-framework/README.md | 37 ++++++++++-- .../src/infrastructure/api-infrastructure.ts | 2 + .../src/infrastructure/e2e-infrastructure.ts | 2 + .../infrastructure/shutdown-handlers.test.ts | 57 +++++++++++++++++++ .../src/pages/adapters/dom-adapter.ts | 7 +++ .../src/serenity/browse-the-web.test.ts | 43 ++++++++++++++ .../src/serenity/browse-the-web.ts | 10 +++- .../src/servers/process-test-server.test.ts | 23 ++++++++ 8 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts create mode 100644 packages/cellix/serenity-framework/src/serenity/browse-the-web.test.ts diff --git a/packages/cellix/serenity-framework/README.md b/packages/cellix/serenity-framework/README.md index a725d434c..219c98ead 100644 --- a/packages/cellix/serenity-framework/README.md +++ b/packages/cellix/serenity-framework/README.md @@ -35,14 +35,19 @@ import { PlaywrightPageAdapter } from '@cellix/serenity-framework/pages/playwrig Compose only the servers a suite needs by chaining `addServer`, then `addUiPortal`, then `finalize`. The framework starts them in dependency order (servers whose `dependsOn` is satisfied start in parallel), calls `resetForScenario()` on servers that implement it, sets up the browser when a UI portal exists, and tears everything down. Each application registers its own set — fewer or more servers — without changing the framework. Once UI portal registration starts, regular server registration is closed; once finalized, no registration methods remain on the typed runtime surface. ```ts +import { fileURLToPath } from 'node:url'; import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; import { MongoMemoryProcessTestServer, ProcessTestServer, ProcessUiTestServer } from '@cellix/serenity-framework/servers'; +const mongoAppPath = fileURLToPath(new URL('../../apps/server-mongodb-memory-mock', import.meta.url)); +const apiAppPath = fileURLToPath(new URL('../../apps/api', import.meta.url)); +const communityAppPath = fileURLToPath(new URL('../../apps/ui-community', import.meta.url)); + const mongo = new MongoMemoryProcessTestServer({ serverName: 'Mongo', executable: 'pnpm', spawnArgs: ['run', 'dev'], - cwd: '/repo/apps/server-mongodb-memory-mock', + cwd: mongoAppPath, connectionString: mongoConnectionString, dbName, portsToCloseBeforeStart: () => mongoPort, @@ -54,7 +59,7 @@ const api = new ProcessTestServer({ serverName: 'Api', executable: 'pnpm', spawnArgs: ['run', process.env.WORKTREE_NAME ? 'dev:worktree' : 'dev'], - cwd: '/repo/apps/api', + cwd: apiAppPath, url: 'https://api.localhost:1355/api/graphql', readyMarker: 'Functions:', }); @@ -63,7 +68,7 @@ const community = new ProcessUiTestServer({ serverName: 'Community portal', executable: 'pnpm', spawnArgs: ['run', 'dev'], - cwd: '/repo/apps/ui-community', + cwd: communityAppPath, url: 'https://community.localhost:1355', readyMarker: 'ready in', }); @@ -91,7 +96,7 @@ const staffContext = await infrastructure.newPortalContext('staff'); Register server objects directly. Dependencies that need one another receive those references through normal object construction; `dependsOn` describes startup order only. Each UI portal owns its browser-context recipe, so `newPortalContext(name)` scopes navigation to that portal without any portal being special-cased. The framework never imports app paths or app-specific environment helpers — pass those into server constructors from the consumer package. -`ProcessTestServer` performs a narrow health check after it sees the ready marker: GraphQL URLs ending in `/graphql` use a small POST `{ __typename }` request, and root web URLs use the fetch default. Service-specific URLs, raw localhost dependencies, and non-HTTP URLs trust the ready marker. +`ProcessTestServer` performs a narrow health check after it sees the ready marker: GraphQL URLs ending in `/graphql` use a small POST `{ __typename }` request, and root web URLs use the fetch default. Service-specific URLs, raw localhost dependencies, and non-HTTP URLs trust the ready marker. In particular, root URLs on `localhost` or `127.0.0.1` are considered ready as soon as the marker appears; no HTTP probe runs for them. ## API acceptance infrastructure @@ -138,6 +143,23 @@ export const ApiWorld = registerManagedSerenityWorld({ }); ``` +DOM-only component suites can use the same managed-world lifecycle without +starting external infrastructure. Pass the smallest infrastructure object that +can initialize the cast: + +```ts +import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast } from '@cellix/serenity-framework/serenity'; + +export const DomWorld = registerManagedSerenityWorld>({ + infrastructure: { + ensureStarted: () => Promise.resolve(), + getState: () => ({}), + }, + createCast: () => new SerenityCast({ useNotepad: true, abilities: [] }), +}); +``` + ## DOM (happy-dom) helpers Component-level acceptance tests run against an in-process DOM provided by @@ -175,3 +197,10 @@ await actor.attemptsTo(Render.component(, { wrapper: withProviders( // task/question: build the page object from the actor's container const page = new LoginPage(new DomPageAdapter(RenderInDom.as(actor).container)); ``` + +`DomPageAdapter` intentionally keeps unresolved element handles inert: `fill`, +`click`, and `check` are no-ops when the selector did not resolve, while +`isVisible()` returns `false` and reads such as `textContent()` return `null`. +Assert that important selectors resolve in DOM-mode tests before relying on +them as a correctness signal. Browser-backed Playwright adapters may throw for +the same unresolved operations. diff --git a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts index 8f5e4e4b0..d2d2d7f9f 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/api-infrastructure.ts @@ -166,6 +166,8 @@ export class ApiInfrastructure implements ApiInfrastructureServerChain, ApiInfra /** Stop every created server, including partial starts, then clean up the suite environment. */ async stopAll(): Promise { + ApiInfrastructure.shutdownTargets.delete(this); + const tracked = new Set(this.startOrder); const createdButUntracked = [...this.created.keys()].filter((name) => !tracked.has(name)).reverse(); const stopOrder = [...createdButUntracked, ...[...this.startOrder].reverse()]; diff --git a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts index 0c18f385d..c530d3ada 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/e2e-infrastructure.ts @@ -259,6 +259,8 @@ export class E2EInfrastructure implements E2EServerRegistrationChain, E2EUiPorta /** Stop browser resources, every created server including partial starts, and the suite environment. */ async stopAll(): Promise { + E2EInfrastructure.shutdownTargets.delete(this); + await this.closeBrowser(); const tracked = new Set(this.startOrder); diff --git a/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts b/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts new file mode 100644 index 000000000..8cb89dffa --- /dev/null +++ b/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { TestServer } from '../servers/index.ts'; + +class FakeServer implements TestServer { + private running = false; + + start(): Promise { + this.running = true; + return Promise.resolve(); + } + + stop(): Promise { + this.running = false; + return Promise.resolve(); + } + + isRunning(): boolean { + return this.running; + } + + getUrl(): string { + return 'process://fake'; + } +} + +describe('infrastructure shutdown handlers', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + }); + + it('removes stopped API infrastructure instances from the process shutdown registry', async () => { + const once = vi.spyOn(process, 'once').mockImplementation(() => process); + const { ApiInfrastructure } = await import('./index.ts'); + + const first = ApiInfrastructure.create().addServer('api', new FakeServer()).finalize(); + expect(once).toHaveBeenCalledTimes(2); + + await first.stopAll(); + + ApiInfrastructure.create().addServer('api', new FakeServer()).finalize(); + expect(once).toHaveBeenCalledTimes(4); + }); + + it('removes stopped E2E infrastructure instances from the process shutdown registry', async () => { + const once = vi.spyOn(process, 'once').mockImplementation(() => process); + const { E2EInfrastructure } = await import('./index.ts'); + + const first = E2EInfrastructure.create().addServer('api', new FakeServer()).finalize(); + expect(once).toHaveBeenCalledTimes(2); + + await first.stopAll(); + + E2EInfrastructure.create().addServer('api', new FakeServer()).finalize(); + expect(once).toHaveBeenCalledTimes(4); + }); +}); diff --git a/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts b/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts index 8ce76571a..e4f533bcd 100644 --- a/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts +++ b/packages/cellix/serenity-framework/src/pages/adapters/dom-adapter.ts @@ -26,6 +26,13 @@ function findLabelControl(container: Element, text: string): Element | null { /** * Element handle backed by an in-process DOM `Element` (happy-dom or jsdom). + * + * Missing selections are represented as `null` elements. Mutating operations + * such as `fill`, `click`, and `check` become no-ops in that state so DOM + * component acceptance tests can share page-object code with browser E2E + * tests. Use `isVisible`, `textContent`, or another assertion before treating a + * selector as proven; Playwright-backed handles still throw for many unresolved + * operations. */ export class DomElementHandle implements ElementHandle { /** diff --git a/packages/cellix/serenity-framework/src/serenity/browse-the-web.test.ts b/packages/cellix/serenity-framework/src/serenity/browse-the-web.test.ts new file mode 100644 index 000000000..ab06c919d --- /dev/null +++ b/packages/cellix/serenity-framework/src/serenity/browse-the-web.test.ts @@ -0,0 +1,43 @@ +import type { BrowserContext, Page } from 'playwright'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { BrowseTheWeb } from './browser.ts'; + +function browserHandles() { + const page = { + close: vi.fn(), + isClosed: vi.fn(() => false), + } as unknown as Page; + const context = { + close: vi.fn(), + } as unknown as BrowserContext; + + return { context, page }; +} + +describe('BrowseTheWeb', () => { + afterEach(async () => { + await BrowseTheWeb.current()?.close(); + }); + + it('clears the fallback browser ability when the active ability closes', async () => { + const { context, page } = browserHandles(); + const ability = BrowseTheWeb.using(page, context); + + expect(BrowseTheWeb.current()).toBe(ability); + + await ability.close(); + + expect(BrowseTheWeb.current()).toBeUndefined(); + }); + + it('keeps the replacement fallback active when an older ability closes', async () => { + const first = browserHandles(); + const second = browserHandles(); + const older = BrowseTheWeb.using(first.page, first.context); + const current = BrowseTheWeb.using(second.page, second.context); + + await older.close(); + + expect(BrowseTheWeb.current()).toBe(current); + }); +}); diff --git a/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts b/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts index 5eb11f69d..1d26e9af2 100644 --- a/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts +++ b/packages/cellix/serenity-framework/src/serenity/browse-the-web.ts @@ -8,7 +8,11 @@ let fallbackInstance: BrowseTheWeb | undefined; * Serenity ability that exposes a Playwright page and browser context. * * The ability supports a current fallback page for single-browser test suites - * and optional actor registration for multi-actor scenarios. + * and optional actor registration for multi-actor scenarios. Calling + * {@link BrowseTheWeb.using} replaces the fallback instance. Closing an ability + * detaches it from the fallback and actor registries; suites that create + * browser abilities directly should close them during teardown to avoid leaking + * actor registrations across scenarios in the same process. */ export class BrowseTheWeb extends Ability { /** Playwright page used by tasks and page adapters. */ @@ -21,6 +25,9 @@ export class BrowseTheWeb extends Ability { * * @param page Playwright page assigned to the ability. * @param context Playwright browser context that owns the page. + * @returns The active browser ability. This also replaces the fallback used by + * {@link BrowseTheWeb.current} and by actors without an actor-specific + * registration. */ static using(page: Page, context: BrowserContext): BrowseTheWeb { const ability = new BrowseTheWeb(page, context); @@ -32,6 +39,7 @@ export class BrowseTheWeb extends Ability { * Register this ability for a named actor. * * @param name Actor name used by Serenity/JS. + * @returns This ability, for fluent cast setup. */ registerForActor(name: string): this { this.actorName = name; diff --git a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts index 32a4a7489..1f1697542 100644 --- a/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts +++ b/packages/cellix/serenity-framework/src/servers/process-test-server.test.ts @@ -125,4 +125,27 @@ describe('ProcessTestServer', () => { fetch.mockRestore(); } }); + + it('trusts the ready marker for localhost root URLs without an HTTP probe', async () => { + const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 500 })); + const server = new ProcessTestServer({ + serverName: 'localhost web server', + executable: process.execPath, + spawnArgs: ['-e', "console.log('READY'); setInterval(() => undefined, 1_000)"], + cwd: process.cwd(), + readyMarker: 'READY', + url: 'http://127.0.0.1:4000/', + shutdownTimeoutMs: 500, + }); + + try { + await server.start(); + + expect(server.isRunning()).toBe(true); + expect(fetch).not.toHaveBeenCalled(); + } finally { + await server.stop(); + fetch.mockRestore(); + } + }); }); From 2c14eecf892cba48d03dbf584f27940bfe98be52 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 23 Jun 2026 10:44:28 -0400 Subject: [PATCH 32/40] retry pipeline due to non explicit error --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 08ee2e5d2..345a2fcb3 100644 --- a/readme.md +++ b/readme.md @@ -186,6 +186,7 @@ The mock auth server now runs as a single instance. Configure it using the follo - PORT — port the HTTP server listens on (default: 1355). For local development you may either use the portless HTTPS proxy with `PORT=443` (recommended for a production-like experience), or run services on the non-privileged port 1355 and include `:1355` in BASE_URL values to avoid TLS trust prompts. - BASE_URL — externally visible origin used as the OIDC issuer (in local development this is typically handled by the portless dev proxy). Example: `https://mock-auth.ownercommunity.localhost` (do not include `:443` when using the standard HTTPS port). When running on port 1355 include the explicit port: `https://mock-auth.ownercommunity.localhost:1355`. + See ADR-0028 — Portless Local Development (apps/docs/docs/decisions/0028-portless-local-development.md) for the canonical proxy start/stop commands. Per-portal port allocation (PORT_BASE) is no longer used. From 7f099e6ebd371c91bb862a7dc1088358778b8a67 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 23 Jun 2026 14:48:33 -0400 Subject: [PATCH 33/40] Address PR #270 review feedback: exit-code clobbering and duplicated e2e detection forwardChildExit unconditionally overwrote process.exitCode, masking an earlier non-zero failure when a wrapper script owns more than one child process. Extracted the azurite runner's setProcessExitCode guard into process/index.ts as a shared helper and reused it from both forwardChildExit and the azurite runner. isE2E was duplicated verbatim in vite/index.ts, azure-functions-local-settings.ts, and @ocom/local-dev-config's api-settings builder. Centralized it in a new env/index.ts module exported from @cellix/local-dev. Also documented WorktreeSettings' value-content URL scanning (vs. the key-based convertSettingsForWorktree) so consumers aren't surprised by a setting value that happens to contain an http(s)/mongodb URL getting rewritten. Co-Authored-By: Claude Sonnet 4.6 --- packages/cellix/local-dev/src/env/index.ts | 17 ++++++++++++++ packages/cellix/local-dev/src/index.ts | 1 + .../cellix/local-dev/src/process/index.ts | 22 ++++++++++++++++--- .../runners/azure-functions-local-settings.ts | 9 +------- .../cellix/local-dev/src/runners/azurite.ts | 8 +------ packages/cellix/local-dev/src/vite/index.ts | 14 ++---------- .../cellix/local-dev/src/worktree/settings.ts | 10 +++++++++ .../src/api-settings/index.ts | 10 +-------- 8 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 packages/cellix/local-dev/src/env/index.ts diff --git a/packages/cellix/local-dev/src/env/index.ts b/packages/cellix/local-dev/src/env/index.ts new file mode 100644 index 000000000..b87ec3b5b --- /dev/null +++ b/packages/cellix/local-dev/src/env/index.ts @@ -0,0 +1,17 @@ +type E2EEnv = NodeJS.ProcessEnv & { + E2E?: string; +}; + +/** + * Returns true when the current process is running in an e2e-oriented mode. + * + * Shared so Vite arg building, Azure Functions local-settings sync, and + * consumer-defined settings builders agree on what counts as "e2e" instead of + * each re-implementing the same truthy-string check. + * + * @param env - Environment to inspect. Defaults to `process.env`. + * @returns Whether `E2E` is enabled with a truthy local-dev value. + */ +export function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { + return ['1', 'true', 'yes'].includes(((env as E2EEnv).E2E ?? '').toLowerCase()); +} diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index eba85b0d6..dc3ff6147 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,3 +1,4 @@ +export { isE2E } from './env/index.ts'; export { type DotEnvValues, readDotEnv } from './files/dotenv.ts'; export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './files/json.ts'; export { diff --git a/packages/cellix/local-dev/src/process/index.ts b/packages/cellix/local-dev/src/process/index.ts index 08177cc5f..536a28cf9 100644 --- a/packages/cellix/local-dev/src/process/index.ts +++ b/packages/cellix/local-dev/src/process/index.ts @@ -8,21 +8,37 @@ export function isGracefulInterruptExit(signal: NodeJS.Signals | null | undefine return signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGQUIT' || code === 130 || code === 143; } +/** + * Sets `process.exitCode` without clobbering a previously recorded failure. + * + * A wrapper script can own more than one child process (for example a runner + * that also forwards an Azurite or Mongo helper's exit). Once any of them has + * recorded a non-zero code, later successful or graceful exits must not erase + * it, or the original failure goes unreported. + */ +export function setProcessExitCode(code: number): void { + if (process.exitCode === undefined || process.exitCode === 0) { + process.exitCode = code; + } +} + /** * Forwards a spawned child process exit code back to the parent process while * treating common interrupt exits as a successful shutdown. * * This helper sets `process.exitCode` instead of calling `process.exit()`, so a * wrapper script can finish naturally and embedders do not lose control of the - * current Node process. + * current Node process. It preserves an already-recorded non-zero exit code + * rather than overwriting it, so a later, unrelated child exiting cleanly does + * not mask an earlier failure. */ export function forwardChildExit(child: ChildProcess): void { child.on('exit', (code, signal) => { if (isGracefulInterruptExit(signal, code)) { - process.exitCode = 0; + setProcessExitCode(0); return; } - process.exitCode = code ?? 1; + setProcessExitCode(code ?? 1); }); } diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts index f5bf3db50..63b6c60b4 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -1,17 +1,10 @@ import path from 'node:path'; +import { isE2E } from '../env/index.ts'; import { writeJsonFile } from '../files/json.ts'; import { convertSettingsForWorktree, type WorktreeConversionPlan } from '../worktree/conversion.ts'; import type { SettingsRecord, WorktreeMode } from '../worktree/types.ts'; import { resolveWorktreeName } from '../worktree/worktree-name.ts'; -type LocalSettingsEnv = NodeJS.ProcessEnv & { - E2E?: string; -}; - -function isE2E(env: NodeJS.ProcessEnv): boolean { - return ['1', 'true', 'yes'].includes(((env as LocalSettingsEnv).E2E ?? '').toLowerCase()); -} - function resolveTargetPath(options: AzureFunctionsLocalSettingsOptions): string { const { appDir = process.cwd(), scriptRoot = 'deploy/' } = options; return options.targetPath ?? path.join(appDir, scriptRoot, 'local.settings.json'); diff --git a/packages/cellix/local-dev/src/runners/azurite.ts b/packages/cellix/local-dev/src/runners/azurite.ts index 53ccbf096..7a6dd853c 100644 --- a/packages/cellix/local-dev/src/runners/azurite.ts +++ b/packages/cellix/local-dev/src/runners/azurite.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from 'node:child_process'; import path from 'node:path'; -import { isGracefulInterruptExit } from '../process/index.ts'; +import { isGracefulInterruptExit, setProcessExitCode } from '../process/index.ts'; import { sanitizeWorktreeHostnameLabel } from '../urls/index.ts'; import { resolveWorkspaceRoot } from '../workspace/index.ts'; import { getAzuritePorts } from '../worktree/ports.ts'; @@ -8,12 +8,6 @@ import { resolveWorktreeName } from '../worktree/worktree-name.ts'; import { spawnInherited } from './spawn.ts'; import type { RunnerOptions } from './types.ts'; -function setProcessExitCode(code: number): void { - if (process.exitCode === undefined || process.exitCode === 0) { - process.exitCode = code; - } -} - function getWorktreeStorageSuffix(worktreeName: string | undefined): string { const safeLabel = sanitizeWorktreeHostnameLabel(worktreeName); return safeLabel ? `-${safeLabel}` : ''; diff --git a/packages/cellix/local-dev/src/vite/index.ts b/packages/cellix/local-dev/src/vite/index.ts index 3adff7888..593be79b4 100644 --- a/packages/cellix/local-dev/src/vite/index.ts +++ b/packages/cellix/local-dev/src/vite/index.ts @@ -1,3 +1,5 @@ +import { isE2E } from '../env/index.ts'; + interface BuildViteArgsOptions { /** Host passed to Vite. Defaults to `127.0.0.1`. */ host?: string; @@ -8,22 +10,10 @@ interface BuildViteArgsOptions { } type ViteEnv = NodeJS.ProcessEnv & { - E2E?: string; E2E_VITE_MODE?: string; TF_BUILD?: string; }; -/** - * Returns true when the current process is running in an e2e-oriented mode. - * - * @param env - Environment to inspect. Defaults to `process.env`. - * @returns Whether `E2E` is enabled with a truthy local-dev value. - */ -function isE2E(env: NodeJS.ProcessEnv = process.env): boolean { - const viteEnv = env as ViteEnv; - return ['1', 'true', 'yes'].includes((viteEnv.E2E ?? '').toLowerCase()); -} - /** * Builds the shared argument list for Vite dev startup across Cellix apps. * diff --git a/packages/cellix/local-dev/src/worktree/settings.ts b/packages/cellix/local-dev/src/worktree/settings.ts index 6af356e6b..8bcaa2240 100644 --- a/packages/cellix/local-dev/src/worktree/settings.ts +++ b/packages/cellix/local-dev/src/worktree/settings.ts @@ -70,6 +70,16 @@ function transformValue(value: unknown, worktreeName: string | undefined): unkno * URL-like strings receive `.localhost` hostname suffixes, MongoDB URLs receive * worktree-specific ports, and `PORT` receives the generic worktree port offset. * + * Unlike {@link convertSettingsForWorktree}, which only touches the keys an + * explicit {@link WorktreeConversionPlan} names, this class scans every string + * value (including nested objects/arrays) for an `http(s)://` or `mongodb://` + * substring and rewrites it wherever found, regardless of key name. This is + * intentional — it's meant for small, app-wrapper-owned `settings` records + * (Vite/Node/Azure Functions dev env), not arbitrary free-text — but a + * setting whose value happens to *contain* such a URL (e.g. a description or + * log line) will be rewritten too. Prefer {@link convertSettingsForWorktree} + * when the settings record may hold unrelated values you don't want touched. + * * @example * ```ts * new WorktreeSettings({ diff --git a/packages/ocom/local-dev-config/src/api-settings/index.ts b/packages/ocom/local-dev-config/src/api-settings/index.ts index 7e384205e..e2aff8c28 100644 --- a/packages/ocom/local-dev-config/src/api-settings/index.ts +++ b/packages/ocom/local-dev-config/src/api-settings/index.ts @@ -1,22 +1,14 @@ import path from 'node:path'; -import type { AzureFunctionsLocalSettingsOptions } from '@cellix/local-dev'; +import { type AzureFunctionsLocalSettingsOptions, isE2E } from '@cellix/local-dev'; import { readJsonFile } from '@cellix/local-dev/files'; import { resolveWorkspaceRoot } from '@cellix/local-dev/workspace'; import type { OcomLocalDevOptions } from '../types.ts'; -type ApiEnv = NodeJS.ProcessEnv & { - E2E?: string; -}; - interface ApiSettingsDocument { Values?: Record; Host?: Record; } -function isE2E(env: NodeJS.ProcessEnv): boolean { - return ['1', 'true', 'yes'].includes(((env as ApiEnv).E2E ?? '').toLowerCase()); -} - /** * The exact api settings that get scoped to a worktree, named explicitly so it * is obvious what changes. Everything else in the settings file is passed From 4b2dcfd66ccce0823d17958f694c88ccceeb7d7d Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 24 Jun 2026 13:25:59 -0400 Subject: [PATCH 34/40] small fixes --- packages/cellix/local-dev/src/index.test.ts | 19 +++---- .../local-dev/src/worktree/conversion.ts | 56 +++++++++---------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index a8a8959eb..ab1d6bddc 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -354,14 +354,14 @@ describe('@cellix/local-dev', () => { }); it('converts only the named settings for a worktree, leaving the rest untouched', () => { + const azuriteConnectionString = + 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=key;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1'; const converted = convertSettingsForWorktree( { ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', - AZURE_STORAGE_CONNECTION_STRING: 'UseDevelopmentStorage=true', - AzureWebJobsStorage: 'UseDevelopmentStorage=true', + AZURE_STORAGE_CONNECTION_STRING: azuriteConnectionString, + AzureWebJobsStorage: azuriteConnectionString, COSMOSDB_DBNAME: 'ocom', }, 'Jason/Feature 123', @@ -374,14 +374,14 @@ describe('@cellix/local-dev', () => { const typedConverted = converted as typeof converted & { COSMOSDB_CONNECTION_STRING: string; }; + const ports = getAzuritePorts('Jason/Feature 123'); expect(converted).toMatchObject({ ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.jason-feature-123.localhost:1355/community', - AZURE_STORAGE_CONNECTION_STRING: expect.stringContaining('BlobEndpoint=http://127.0.0.1:'), - AzureWebJobsStorage: expect.stringContaining('QueueEndpoint=http://127.0.0.1:'), + AZURE_STORAGE_CONNECTION_STRING: expect.stringContaining(`BlobEndpoint=http://127.0.0.1:${ports.blob}/devstoreaccount1`), + AzureWebJobsStorage: expect.stringContaining(`QueueEndpoint=http://127.0.0.1:${ports.queue}/devstoreaccount1`), // Unlisted keys are passed through untouched. COSMOSDB_DBNAME: 'ocom', - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', }); expect(typedConverted.COSMOSDB_CONNECTION_STRING).toBe(`mongodb://127.0.0.1:${getMongoPort('Jason/Feature 123')}/ocom`); }); @@ -404,10 +404,9 @@ describe('@cellix/local-dev', () => { localSettings: { appDir, values: { - STORAGE_ACCOUNT_NAME: 'devstoreaccount1', - STORAGE_ACCOUNT_KEY: 'key', COSMOSDB_CONNECTION_STRING: 'mongodb://127.0.0.1:50000/ocom', - AzureWebJobsStorage: 'UseDevelopmentStorage=true', + AzureWebJobsStorage: + 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=key;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1', ACCOUNT_PORTAL_OIDC_ISSUER: 'https://mock-auth.ownercommunity.localhost:1355/community', languageWorkers__node__arguments: '--inspect=5858', }, diff --git a/packages/cellix/local-dev/src/worktree/conversion.ts b/packages/cellix/local-dev/src/worktree/conversion.ts index 29f3a562a..f6aa4b817 100644 --- a/packages/cellix/local-dev/src/worktree/conversion.ts +++ b/packages/cellix/local-dev/src/worktree/conversion.ts @@ -1,5 +1,5 @@ import { applyWorktreeSuffix, replaceUrlPort } from '../urls/index.ts'; -import { buildAzuriteConnectionString, getAzuritePorts, getMongoPort } from './ports.ts'; +import { getAzuritePorts, getMongoPort } from './ports.ts'; import type { SettingsRecord } from './types.ts'; /** @@ -14,18 +14,13 @@ export interface WorktreeConversionPlan { /** Keys whose `mongodb://` URL value gets the worktree Mongo port. */ mongoKeys?: string[]; /** - * Keys that receive a worktree-scoped Azurite connection string, built from - * the same settings record's `STORAGE_ACCOUNT_NAME` and - * `STORAGE_ACCOUNT_KEY` values. When either credential is missing, these keys - * are left untouched. + * Keys whose Azure Storage connection string value has its `BlobEndpoint`, + * `QueueEndpoint`, and `TableEndpoint` ports replaced with worktree-scoped + * ports. Values without explicit endpoint segments are left untouched. */ azuriteKeys?: string[]; } -/** - * Suffixes a URL's hostname with the worktree label, preserving the rest of the - * URL (path, query, and absence of a trailing slash). - */ function suffixUrlHostname(value: string, worktreeName: string): string { try { const url = new URL(value); @@ -41,26 +36,29 @@ function suffixUrlHostname(value: string, worktreeName: string): string { } } -function buildWorktreeAzuriteConnectionString(values: SettingsRecord, worktreeName: string): string | undefined { - // biome-ignore lint/complexity/useLiteralKeys: noPropertyAccessFromIndexSignature requires bracket notation - const accountName = String(values['STORAGE_ACCOUNT_NAME'] ?? ''); - // biome-ignore lint/complexity/useLiteralKeys: noPropertyAccessFromIndexSignature requires bracket notation - const accountKey = String(values['STORAGE_ACCOUNT_KEY'] ?? ''); - if (!accountName || !accountKey) { - return undefined; - } - - return buildAzuriteConnectionString({ - accountName, - accountKey, - ports: getAzuritePorts(worktreeName), - }); +function replaceAzuriteConnectionStringPorts(value: string, worktreeName: string): string { + const ports = getAzuritePorts(worktreeName); + return value + .split(';') + .map((segment) => { + if (segment.startsWith('BlobEndpoint=')) { + return `BlobEndpoint=${replaceUrlPort(segment.slice('BlobEndpoint='.length), ports.blob)}`; + } + if (segment.startsWith('QueueEndpoint=')) { + return `QueueEndpoint=${replaceUrlPort(segment.slice('QueueEndpoint='.length), ports.queue)}`; + } + if (segment.startsWith('TableEndpoint=')) { + return `TableEndpoint=${replaceUrlPort(segment.slice('TableEndpoint='.length), ports.table)}`; + } + return segment; + }) + .join(';'); } /** * Returns a copy of `values` with the worktree conversion applied to exactly the * keys named in `plan` — URL hostnames suffixed, Mongo ports shifted, and - * Azurite storage keys replaced with a worktree-scoped connection string. + * Azurite endpoint ports replaced with worktree-scoped ports. * * @param values - Settings to convert (not mutated). * @param worktreeName - Active worktree label. @@ -84,12 +82,10 @@ export function convertSettingsForWorktree(values: SettingsRecord, worktreeName: } } - if (plan.azuriteKeys?.length) { - const connectionString = buildWorktreeAzuriteConnectionString(converted, worktreeName); - if (connectionString) { - for (const key of plan.azuriteKeys) { - converted[key] = connectionString; - } + for (const key of plan.azuriteKeys ?? []) { + const value = converted[key]; + if (typeof value === 'string') { + converted[key] = replaceAzuriteConnectionStringPorts(value, worktreeName); } } From a9f581cd8a78329e1bc2c53f498ebb13098a0674 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 24 Jun 2026 13:41:27 -0400 Subject: [PATCH 35/40] security fix --- packages/cellix/local-dev/src/urls/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cellix/local-dev/src/urls/index.ts b/packages/cellix/local-dev/src/urls/index.ts index 78a2ec514..6c9b4f719 100644 --- a/packages/cellix/local-dev/src/urls/index.ts +++ b/packages/cellix/local-dev/src/urls/index.ts @@ -24,7 +24,8 @@ export function sanitizeWorktreeHostnameLabel(worktreeName: string | undefined): const label = worktreeName ?.toLowerCase() .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') + .replace(/^-+/, '') + .replace(/-+$/, '') .slice(0, 63) .replace(/-+$/g, ''); From ad4a95acc34ca80bb621eade2feabb85dfae5567 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 24 Jun 2026 13:54:09 -0400 Subject: [PATCH 36/40] second attempt for security fix --- packages/cellix/local-dev/src/urls/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cellix/local-dev/src/urls/index.ts b/packages/cellix/local-dev/src/urls/index.ts index 6c9b4f719..f39b4f865 100644 --- a/packages/cellix/local-dev/src/urls/index.ts +++ b/packages/cellix/local-dev/src/urls/index.ts @@ -24,10 +24,10 @@ export function sanitizeWorktreeHostnameLabel(worktreeName: string | undefined): const label = worktreeName ?.toLowerCase() .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+/, '') - .replace(/-+$/, '') + .replace(/^-/, '') + .replace(/-$/, '') .slice(0, 63) - .replace(/-+$/g, ''); + .replace(/-$/, ''); return label || undefined; } From cbd2a9d1bba9f17d48d0906f973d4bc0773b3b0c Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 26 Jun 2026 11:16:28 -0400 Subject: [PATCH 37/40] small adjustments based on gpt 5.5 internal review, fix import --- packages/cellix/local-dev/src/index.test.ts | 25 +++++++++++++++++++ .../cellix/local-dev/src/runners/azurite.ts | 7 +++--- .../infrastructure/shutdown-handlers.test.ts | 4 +-- pnpm-lock.yaml | 16 ++++++------ pnpm-workspace.yaml | 4 +-- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index ab1d6bddc..5722f39e9 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -281,6 +281,31 @@ describe('@cellix/local-dev', () => { }); }); + it('starts Azurite with API version checks disabled for newer Azure SDK clients', () => { + const calls: Array<{ command: string; args: string[] }> = []; + const spawn: RunnerSpawn = (command, args) => { + calls.push({ command, args }); + return createMockChildProcess(); + }; + + new AzuriteDevRunner({ spawn }).start(); + + expect(calls).toEqual([ + expect.objectContaining({ + command: 'azurite-blob', + args: expect.arrayContaining(['--skipApiVersionCheck']), + }), + expect.objectContaining({ + command: 'azurite-queue', + args: expect.arrayContaining(['--skipApiVersionCheck']), + }), + expect.objectContaining({ + command: 'azurite-table', + args: expect.arrayContaining(['--skipApiVersionCheck']), + }), + ]); + }); + it('sets exitCode instead of terminating the parent process when Azurite spawn fails', () => { const children: ReturnType[] = []; const spawn: RunnerSpawn = () => { diff --git a/packages/cellix/local-dev/src/runners/azurite.ts b/packages/cellix/local-dev/src/runners/azurite.ts index 7a6dd853c..1fe7f15ea 100644 --- a/packages/cellix/local-dev/src/runners/azurite.ts +++ b/packages/cellix/local-dev/src/runners/azurite.ts @@ -91,10 +91,11 @@ export class AzuriteDevRunner { public start(): ChildProcess[] { const resolved = this.resolveOptions(); const silent = this.options.silent ?? true; + const commonArgs = [...(silent ? ['--silent'] : []), '--skipApiVersionCheck']; const azuriteProcesses: Array<[string, string[]]> = [ - ['azurite-blob', [...(silent ? ['--silent'] : []), '--blobPort', String(resolved.blobPort), '--location', path.resolve(resolved.blobLocation)]], - ['azurite-queue', [...(silent ? ['--silent'] : []), '--queuePort', String(resolved.queuePort), '--location', path.resolve(resolved.queueLocation)]], - ['azurite-table', [...(silent ? ['--silent'] : []), '--tablePort', String(resolved.tablePort), '--location', path.resolve(resolved.tableLocation)]], + ['azurite-blob', [...commonArgs, '--blobPort', String(resolved.blobPort), '--location', path.resolve(resolved.blobLocation)]], + ['azurite-queue', [...commonArgs, '--queuePort', String(resolved.queuePort), '--location', path.resolve(resolved.queueLocation)]], + ['azurite-table', [...commonArgs, '--tablePort', String(resolved.tablePort), '--location', path.resolve(resolved.tableLocation)]], ]; let procs: ChildProcess[] = []; diff --git a/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts b/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts index 8cb89dffa..94c69e6fe 100644 --- a/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts +++ b/packages/cellix/serenity-framework/src/infrastructure/shutdown-handlers.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { TestServer } from '../servers/index.ts'; +import { ApiInfrastructure, E2EInfrastructure } from './index.ts'; class FakeServer implements TestServer { private running = false; @@ -26,12 +27,10 @@ class FakeServer implements TestServer { describe('infrastructure shutdown handlers', () => { afterEach(() => { vi.restoreAllMocks(); - vi.resetModules(); }); it('removes stopped API infrastructure instances from the process shutdown registry', async () => { const once = vi.spyOn(process, 'once').mockImplementation(() => process); - const { ApiInfrastructure } = await import('./index.ts'); const first = ApiInfrastructure.create().addServer('api', new FakeServer()).finalize(); expect(once).toHaveBeenCalledTimes(2); @@ -44,7 +43,6 @@ describe('infrastructure shutdown handlers', () => { it('removes stopped E2E infrastructure instances from the process shutdown registry', async () => { const once = vi.spyOn(process, 'once').mockImplementation(() => process); - const { E2EInfrastructure } = await import('./index.ts'); const first = E2EInfrastructure.create().addServer('api', new FakeServer()).finalize(); expect(once).toHaveBeenCalledTimes(2); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fae69fc4..6a5374d14 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,11 +164,11 @@ overrides: '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 js-yaml@4.1.1: 4.2.0 - shell-quote@<1.8.4: 1.8.4 + shell-quote@<1.8.4: 1.9.0 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 ws: 8.21.0 - shell-quote: 1.8.4 + shell-quote: 1.9.0 '@grpc/grpc-js': '>=1.14.4' form-data: ^4.0.6 webpack-dev-server>http-proxy-middleware: 3.0.7 @@ -12612,8 +12612,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.4: - resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + shell-quote@1.9.0: + resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} shimmer@1.2.1: @@ -17016,7 +17016,7 @@ snapshots: listr2: 4.0.5 log-symbols: 4.1.0 micromatch: 4.0.8 - shell-quote: 1.8.4 + shell-quote: 1.9.0 string-env-interpolation: 1.0.1 ts-log: 2.2.7 tslib: 2.8.1 @@ -21016,7 +21016,7 @@ snapshots: dependencies: chalk: 4.1.2 rxjs: 7.8.2 - shell-quote: 1.8.4 + shell-quote: 1.9.0 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 @@ -23271,7 +23271,7 @@ snapshots: launch-editor@2.14.1: dependencies: picocolors: 1.1.1 - shell-quote: 1.8.4 + shell-quote: 1.9.0 less@4.4.2: dependencies: @@ -26208,7 +26208,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.4: {} + shell-quote@1.9.0: {} shimmer@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 484f254a9..437990445 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -135,11 +135,11 @@ overrides: '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 'js-yaml@4.1.1': 4.2.0 - 'shell-quote@<1.8.4': 1.8.4 + 'shell-quote@<1.8.4': 1.9.0 '@opentelemetry/exporter-prometheus@0.57.2': 0.217.0 '@opentelemetry/core@2.7.1': 2.8.0 ws: 8.21.0 - shell-quote: 1.8.4 + shell-quote: 1.9.0 '@grpc/grpc-js': '>=1.14.4' form-data: ^4.0.6 'webpack-dev-server>http-proxy-middleware': 3.0.7 From dd56e3105407ba98f9b4d1a3bd8ec484db8fcf33 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 29 Jun 2026 16:23:13 -0400 Subject: [PATCH 38/40] removal of redudant flag for worktree in worktree command --- apps/api/package.json | 4 ++-- packages/cellix/local-dev/README.md | 8 ++++---- packages/cellix/local-dev/manifest.md | 2 +- packages/cellix/local-dev/src/index.test.ts | 5 +++-- .../runners/azure-functions-local-settings.ts | 2 +- .../local-dev/src/runners/azure-functions.ts | 5 ++--- .../cellix/local-dev/src/worktree/types.ts | 4 +--- .../local-dev/src/worktree/worktree-name.ts | 19 +++---------------- pnpm-lock.yaml | 18 +++++++++--------- pnpm-workspace.yaml | 2 +- 10 files changed, 27 insertions(+), 42 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index cef71f712..9702bbf82 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,9 +9,9 @@ "prebuild": "pnpm run lint", "build": "tsgo --build && tsgo --noEmit --project tsconfig.rolldown.json && rolldown -c rolldown.config.ts", "predev": "pnpm run prepare:deploy", - "dev": "CELLIX_WORKTREE=0 pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.ts", + "dev": "pnpm exec portless data-access.ownercommunity.localhost --force node start-dev.ts", "predev:worktree": "pnpm run prepare:deploy", - "dev:worktree": "CELLIX_WORKTREE=1 pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", + "dev:worktree": "pnpm exec portless data-access.ownercommunity.${WORKTREE_NAME}.localhost --force node start-dev.ts", "prepare:deploy": "cellix-prepare-func-deploy", "watch": "tsgo --watch", "test": "vitest run --silent --reporter=dot", diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 6bc81aaa7..735381709 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -99,10 +99,10 @@ explicit `worktreeName`) is present. Regular E2E and dev runs keep the base values. Worktree transforms are enabled when a worktree name is available, either through -`worktreeName` or `WORKTREE_NAME`. Set `worktree: false` or -`CELLIX_WORKTREE=0` for regular dev scripts that should ignore an ambient -`WORKTREE_NAME`; set `worktree: true` or `CELLIX_WORKTREE=1` for explicit -worktree scripts. +`worktreeName` or `WORKTREE_NAME`. This lets regular and worktree package scripts +share the same wrapper: the root `dev:worktree` command supplies `WORKTREE_NAME`, +while regular `dev` does not. Advanced programmatic callers can pass +`worktree: false` to ignore an ambient worktree name. ## Public API diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index 23e5ba0a1..466672710 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -47,7 +47,7 @@ Root entrypoint exports: - App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and which settings values are passed into the worktree transformer. - This package should expose only reusable mechanics that make those wrappers smaller and more consistent. - Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, hostname suffixing, URL-like env values, and JSON settings aligned across all participating apps. -- Worktree transforms are explicit when `worktree` or `CELLIX_WORKTREE` is set; otherwise helpers fall back to applying transforms only when a worktree name is available. +- Worktree transforms are auto-detected from `WORKTREE_NAME`; callers can still override that decision programmatically with `worktree` when needed. - Azure Functions dev runners may prepare `local.settings.json` before startup because the Functions host reads settings from its script root rather than the process environment alone. - Consumers that need the same Azure Functions values outside `func start` can resolve them without writing a file; regular mode remains unscoped unless a worktree name is explicitly available. - Hostname suffixing must sanitize raw worktree names before inserting them into `.localhost` domains, and repeated suffixing with the same worktree label must leave hostnames unchanged. diff --git a/packages/cellix/local-dev/src/index.test.ts b/packages/cellix/local-dev/src/index.test.ts index 0d87010bf..48d60d4b9 100644 --- a/packages/cellix/local-dev/src/index.test.ts +++ b/packages/cellix/local-dev/src/index.test.ts @@ -211,9 +211,10 @@ describe('@cellix/local-dev', () => { }); }); - it('does not apply worktree transforms when CELLIX_WORKTREE disables them', () => { + it('allows programmatic callers to disable worktree transforms explicitly', () => { const env = new WorktreeSettings({ - env: { CELLIX_WORKTREE: '0', WORKTREE_NAME: 'Jason/Feature 123' }, + env: { WORKTREE_NAME: 'Jason/Feature 123' }, + worktree: false, settings: { BASE_URL: 'https://mock-auth.ownercommunity.localhost:1355', PORT: '50000', diff --git a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts index c15f4e2d7..80ddc6fcf 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions-local-settings.ts @@ -37,7 +37,7 @@ export interface AzureFunctionsLocalSettingsOptions { scriptRoot?: string; /** Base environment. Defaults to `process.env`. */ env?: NodeJS.ProcessEnv; - /** Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, then auto mode. */ + /** Whether to apply worktree transforms. Defaults to auto-detection from `WORKTREE_NAME`. */ worktree?: WorktreeMode; /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ worktreeName?: string; diff --git a/packages/cellix/local-dev/src/runners/azure-functions.ts b/packages/cellix/local-dev/src/runners/azure-functions.ts index eacf92a64..4603948dc 100644 --- a/packages/cellix/local-dev/src/runners/azure-functions.ts +++ b/packages/cellix/local-dev/src/runners/azure-functions.ts @@ -17,9 +17,8 @@ type RunnerEnv = NodeJS.ProcessEnv & { export interface AzureFunctionsDevOptions extends EnvRunnerOptions { /** - * Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, - * then auto mode. The api is the only app that toggles this: its plain `dev` - * sets `CELLIX_WORKTREE=0` so settings sync without worktree scoping. + * Whether to apply worktree transforms. Defaults to auto-detection from + * `WORKTREE_NAME`. */ worktree?: WorktreeMode; /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ diff --git a/packages/cellix/local-dev/src/worktree/types.ts b/packages/cellix/local-dev/src/worktree/types.ts index eca3c9a77..99f73e7c0 100644 --- a/packages/cellix/local-dev/src/worktree/types.ts +++ b/packages/cellix/local-dev/src/worktree/types.ts @@ -19,8 +19,6 @@ export type KnownWorktreeSettings = SettingsRecord & { * Environment shape used by worktree helpers. */ export type WorktreeEnv = NodeJS.ProcessEnv & { - /** Explicit toggle for worktree transforms. */ - CELLIX_WORKTREE?: string; /** Raw worktree name used for hostname suffixes and port offsets. */ WORKTREE_NAME?: string; }; @@ -41,7 +39,7 @@ export interface WorktreeSettingsOptions { env?: NodeJS.ProcessEnv; /** Settings to merge over `env` before worktree transforms are applied. */ settings?: SettingsRecord; - /** Whether to apply worktree transforms. Defaults to `env.CELLIX_WORKTREE`, then auto mode. */ + /** Whether to apply worktree transforms. Defaults to auto-detection from `WORKTREE_NAME`. */ worktree?: WorktreeMode; /** Worktree name. Defaults to `env.WORKTREE_NAME`. */ worktreeName?: string; diff --git a/packages/cellix/local-dev/src/worktree/worktree-name.ts b/packages/cellix/local-dev/src/worktree/worktree-name.ts index 8cbfbbc45..0b8b572e5 100644 --- a/packages/cellix/local-dev/src/worktree/worktree-name.ts +++ b/packages/cellix/local-dev/src/worktree/worktree-name.ts @@ -1,21 +1,10 @@ import type { WorktreeEnv, WorktreeMode } from './types.ts'; -function parseWorktreeMode(value: string | undefined): WorktreeMode | undefined { - if (!value) return undefined; - - const normalized = value.toLowerCase(); - if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; - if (['0', 'false', 'no', 'off'].includes(normalized)) return false; - - return undefined; -} - /** * Resolves the effective worktree name for a local-dev operation. * - * `worktree: false` or `CELLIX_WORKTREE=0` disables worktree behavior even - * when `WORKTREE_NAME` is present. Invalid `CELLIX_WORKTREE` values are ignored - * so callers can fall back to automatic mode. + * A `WORKTREE_NAME` enables worktree behavior automatically. Callers can pass + * `worktree: false` to disable it explicitly. * * @param options - Optional env, explicit mode, and explicit worktree name. * @returns The worktree name to apply, or `undefined` when transforms should be @@ -23,9 +12,7 @@ function parseWorktreeMode(value: string | undefined): WorktreeMode | undefined */ export function resolveWorktreeName(options: { env?: NodeJS.ProcessEnv; worktree?: WorktreeMode; worktreeName?: string }): string | undefined { const env = (options.env ?? process.env) as WorktreeEnv; - const worktreeMode = options.worktree ?? parseWorktreeMode(env.CELLIX_WORKTREE); - - if (worktreeMode === false) return undefined; + if (options.worktree === false) return undefined; return options.worktreeName ?? env.WORKTREE_NAME; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b9c6151e..bc421ef5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,7 +160,7 @@ overrides: postcss: 8.5.10 protobufjs: 7.6.3 ip-address: ^10.1.1 - fast-uri: ^3.1.2 + fast-uri: 4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 js-yaml@4.1.1: 4.2.0 @@ -9126,8 +9126,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@4.0.1: + resolution: {integrity: sha512-j0VntHrljgRKiWdbjL1xECG44OQu5sb7en4b8z2CcRvtDUEG5DAvkXlllrRhvbABR+m2mUiSgYBPvDj/PzB0dg==} fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -16046,7 +16046,7 @@ snapshots: '@cucumber/gherkin-utils': 11.0.0 '@cucumber/html-formatter': 23.0.0(@cucumber/messages@32.2.0) '@cucumber/junit-xml-formatter': 0.13.3(@cucumber/messages@32.2.0) - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 '@cucumber/pretty-formatter': 1.0.1(@cucumber/cucumber@12.8.1)(@cucumber/messages@32.2.0) '@cucumber/tag-expressions': 9.1.0 @@ -16082,7 +16082,7 @@ snapshots: '@cucumber/gherkin-streams@6.0.0(@cucumber/gherkin@38.0.0)(@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0))(@cucumber/messages@32.2.0)': dependencies: '@cucumber/gherkin': 38.0.0 - '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.2.0) + '@cucumber/message-streams': 4.1.1(@cucumber/messages@32.3.1) '@cucumber/messages': 32.2.0 commander: 14.0.0 source-map-support: 0.5.21 @@ -16131,9 +16131,9 @@ snapshots: luxon: 3.7.2 xmlbuilder: 15.1.1 - '@cucumber/message-streams@4.1.1(@cucumber/messages@32.2.0)': + '@cucumber/message-streams@4.1.1(@cucumber/messages@32.3.1)': dependencies: - '@cucumber/messages': 32.2.0 + '@cucumber/messages': 32.3.1 mime: 3.0.0 '@cucumber/messages@26.0.1': @@ -20217,7 +20217,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 4.0.1 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -22064,7 +22064,7 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-uri@3.1.2: {} + fast-uri@4.0.1: {} fast-xml-builder@1.2.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9ab530518..7cff55da5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -131,7 +131,7 @@ overrides: postcss: 8.5.10 protobufjs: 7.6.3 ip-address: ^10.1.1 - fast-uri: ^3.1.2 + fast-uri: 4.0.1 '@babel/plugin-transform-modules-systemjs': 7.29.4 '@babel/core': ^7.29.6 'js-yaml@4.1.1': 4.2.0 From 0dd81fc0db9f164ba92ed4787a007aee9934d9c4 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 30 Jun 2026 10:05:09 -0400 Subject: [PATCH 39/40] fix vulnerability --- .snyk | 5 +++++ pnpm-lock.yaml | 22 ++++++++++++++-------- pnpm-workspace.yaml | 4 +--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.snyk b/.snyk index ab7d0796e..1e83d06a5 100644 --- a/.snyk +++ b/.snyk @@ -168,3 +168,8 @@ ignore: reason: 'The fix requires migrating the Azure Functions telemetry stack from OpenTelemetry 1.x to 2.8.0 or newer. The current Azure exporter and 0.57.x SDK packages require the 1.x API family; a forced major override is type-compatible at build time but leaves mixed SDK internals. Accepted temporarily while the existing OTEL 2.x migration is completed.' expires: '2026-07-18T00:00:00.000Z' created: '2026-06-18T00:00:00.000Z' + 'SNYK-JS-BRACEEXPANSION-17706650': + - '* > brace-expansion@1.1.13': + reason: 'Transitive in Docusaurus serve-handler for local static docs serving. The fixed brace-expansion 5.x line requires minimatch 10, whose named-export API is incompatible with serve-handler 6.1.7.' + expires: '2026-09-30T00:00:00.000Z' + created: '2026-06-30T00:00:00.000Z' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6af15ca7c..786a30593 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,9 +132,7 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - brace-expansion@1.1.12: 1.1.13 - brace-expansion@5.0.4: 5.0.6 - brace-expansion@5.0.5: 5.0.6 + brace-expansion@5: 5.0.7 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -7909,8 +7907,8 @@ packages: brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -10924,6 +10922,10 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -19541,7 +19543,7 @@ snapshots: '@ts-morph/common@0.28.1': dependencies: - minimatch: 10.2.4 + minimatch: 10.2.5 path-browserify: 1.0.1 tinyglobby: 0.2.17 @@ -20700,7 +20702,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -24274,7 +24276,11 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a5e8c4715..470e82a01 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -80,9 +80,7 @@ overrides: jiti: 2.6.1 rollup: ^4.59.0 '@ant-design/pro-layout>path-to-regexp': ^8.4.0 - 'brace-expansion@1.1.12': 1.1.13 - 'brace-expansion@5.0.4': 5.0.6 - 'brace-expansion@5.0.5': 5.0.6 + 'brace-expansion@5': 5.0.7 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 From 232c87b3958911e299d472354f5884cbf285189f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 1 Jul 2026 09:51:53 -0400 Subject: [PATCH 40/40] small sourcery feedback adjustment, and small fix for storybook creating main.tmp --- packages/cellix/local-dev/README.md | 5 +++++ packages/cellix/local-dev/manifest.md | 2 +- packages/cellix/local-dev/package.json | 4 ++++ packages/cellix/local-dev/src/index.ts | 2 ++ .../cellix/local-dev/src/worktree/settings.ts | 20 ++++++++----------- .../.storybook/main.ts | 3 +++ 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/cellix/local-dev/README.md b/packages/cellix/local-dev/README.md index 735381709..06d0d6ebe 100644 --- a/packages/cellix/local-dev/README.md +++ b/packages/cellix/local-dev/README.md @@ -104,6 +104,11 @@ share the same wrapper: the root `dev:worktree` command supplies `WORKTREE_NAME` while regular `dev` does not. Advanced programmatic callers can pass `worktree: false` to ignore an ambient worktree name. +`WorktreeSettings` transforms complete `http(s)://` and `mongodb://` string +values, including complete URL values nested in objects or arrays. It leaves URLs +embedded in descriptive text unchanged. Use `convertSettingsForWorktree` when +the application needs explicit key-level conversion policy. + ## Public API All exports are available from `@cellix/local-dev`. Folder-level subpaths are diff --git a/packages/cellix/local-dev/manifest.md b/packages/cellix/local-dev/manifest.md index 466672710..5e75d69ea 100644 --- a/packages/cellix/local-dev/manifest.md +++ b/packages/cellix/local-dev/manifest.md @@ -47,7 +47,7 @@ Root entrypoint exports: - App packages own local-development policy such as env-variable names, URL mappings, auth-provider routes, and which settings values are passed into the worktree transformer. - This package should expose only reusable mechanics that make those wrappers smaller and more consistent. - Worktree isolation is deterministic and should keep MongoDB ports, Azurite ports, hostname suffixing, URL-like env values, and JSON settings aligned across all participating apps. -- Worktree transforms are auto-detected from `WORKTREE_NAME`; callers can still override that decision programmatically with `worktree` when needed. +- Worktree transforms are auto-detected from `WORKTREE_NAME`; callers can still override that decision programmatically with `worktree` when needed. Generic settings transforms apply only to complete URL-valued strings, while explicit key-level policy belongs in `convertSettingsForWorktree`. - Azure Functions dev runners may prepare `local.settings.json` before startup because the Functions host reads settings from its script root rather than the process environment alone. - Consumers that need the same Azure Functions values outside `func start` can resolve them without writing a file; regular mode remains unscoped unless a worktree name is explicitly available. - Hostname suffixing must sanitize raw worktree names before inserting them into `.localhost` domains, and repeated suffixing with the same worktree label must leave hostnames unchanged. diff --git a/packages/cellix/local-dev/package.json b/packages/cellix/local-dev/package.json index 43addae60..b16991873 100644 --- a/packages/cellix/local-dev/package.json +++ b/packages/cellix/local-dev/package.json @@ -18,6 +18,10 @@ "types": "./dist/files/index.d.ts", "default": "./dist/files/index.js" }, + "./process": { + "types": "./dist/process/index.d.ts", + "default": "./dist/process/index.js" + }, "./runners": { "types": "./dist/runners/index.d.ts", "default": "./dist/runners/index.js" diff --git a/packages/cellix/local-dev/src/index.ts b/packages/cellix/local-dev/src/index.ts index a7aef1d82..7dbfeb397 100644 --- a/packages/cellix/local-dev/src/index.ts +++ b/packages/cellix/local-dev/src/index.ts @@ -1,6 +1,7 @@ export { isE2E } from './env/index.ts'; export { type DotEnvValues, readDotEnv } from './files/dotenv.ts'; export { readJsonFile, type SyncJsonFileOptions, syncJsonFile } from './files/json.ts'; +export { forwardChildExit, isGracefulInterruptExit, setProcessExitCode } from './process/index.ts'; export { type AzureFunctionsDevOptions, AzureFunctionsDevRunner, @@ -27,6 +28,7 @@ export { replaceUrlPort, sanitizeWorktreeHostnameLabel, } from './urls/index.ts'; +export { buildViteArgs } from './vite/index.ts'; export { type ResolveWorkspaceRootOptions, resolveWorkspaceRoot } from './workspace/index.ts'; export { convertSettingsForWorktree, diff --git a/packages/cellix/local-dev/src/worktree/settings.ts b/packages/cellix/local-dev/src/worktree/settings.ts index 8bcaa2240..6b5773a25 100644 --- a/packages/cellix/local-dev/src/worktree/settings.ts +++ b/packages/cellix/local-dev/src/worktree/settings.ts @@ -3,8 +3,7 @@ import { getMongoPort, getWorktreePortOffset } from './ports.ts'; import type { KnownWorktreeSettings, SettingsRecord, WorktreeSettingsOptions } from './types.ts'; import { resolveWorktreeName } from './worktree-name.ts'; -const HTTP_URL_PATTERN = /https?:\/\/[^\s;'"<>]+/g; -const MONGO_URL_PATTERN = /mongodb:\/\/[^\s;'"<>]+/g; +const TRANSFORMABLE_URL_PATTERN = /^(?:https?:\/\/|mongodb:\/\/)[^\s]+$/; function isPlainRecord(value: unknown): value is SettingsRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -43,8 +42,7 @@ function transformUrl(urlValue: string, worktreeName: string | undefined): strin } function transformStringValue(value: string, worktreeName: string | undefined): string { - const withHttpUrls = value.replace(HTTP_URL_PATTERN, (match) => transformUrl(match, worktreeName)); - return withHttpUrls.replace(MONGO_URL_PATTERN, (match) => transformUrl(match, worktreeName)); + return TRANSFORMABLE_URL_PATTERN.test(value) ? transformUrl(value, worktreeName) : value; } function transformValue(value: unknown, worktreeName: string | undefined): unknown { @@ -71,14 +69,12 @@ function transformValue(value: unknown, worktreeName: string | undefined): unkno * worktree-specific ports, and `PORT` receives the generic worktree port offset. * * Unlike {@link convertSettingsForWorktree}, which only touches the keys an - * explicit {@link WorktreeConversionPlan} names, this class scans every string - * value (including nested objects/arrays) for an `http(s)://` or `mongodb://` - * substring and rewrites it wherever found, regardless of key name. This is - * intentional — it's meant for small, app-wrapper-owned `settings` records - * (Vite/Node/Azure Functions dev env), not arbitrary free-text — but a - * setting whose value happens to *contain* such a URL (e.g. a description or - * log line) will be rewritten too. Prefer {@link convertSettingsForWorktree} - * when the settings record may hold unrelated values you don't want touched. + * explicit {@link WorktreeConversionPlan} names, this class checks every string + * value (including nested objects/arrays). Only values that begin with a + * supported URL scheme and parse as a complete `http(s)://` or `mongodb://` + * URL are rewritten. URLs embedded in descriptions or log lines are left + * untouched. Prefer {@link convertSettingsForWorktree} when callers need + * explicit key-level policy. * * @example * ```ts diff --git a/packages/ocom/ui-staff-route-user-management/.storybook/main.ts b/packages/ocom/ui-staff-route-user-management/.storybook/main.ts index 055dca4e5..89a44ade1 100644 --- a/packages/ocom/ui-staff-route-user-management/.storybook/main.ts +++ b/packages/ocom/ui-staff-route-user-management/.storybook/main.ts @@ -1,6 +1,9 @@ +import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import type { StorybookConfig } from '@storybook/react-vite'; +const require = createRequire(import.meta.url); + function getAbsolutePath(value: string) { return dirname(require.resolve(join(value, 'package.json'))); }