From 09f585c8881b3ebd1ab6ae0e8261dabb0212cd6c Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 1 Jul 2026 10:02:19 -0400 Subject: [PATCH 01/10] Arch unit test additions - initial commit --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 345a2fcb3..6800a68c5 100644 --- a/readme.md +++ b/readme.md @@ -146,6 +146,7 @@ pnpm run build ``` + Mock OIDC server The local mock OpenID Connect provider used for UI development is configured per-portal via JSON files placed in the UI app directories. From 77002a485c4de9004ac08992b9f64dda6270dab1 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 1 Jul 2026 14:37:30 -0400 Subject: [PATCH 02/10] arch unit test additions, and additional stub testing to assert negative test results --- .snyk | 5 + apps/api/package.json | 1 + .../archunit-tests/api-composition.test.ts | 5 + apps/ui-community/mock-oidc.users.json | 6 +- apps/ui-community/package.json | 2 + .../archunit-tests/ui-app-composition.test.ts | 6 + apps/ui-community/vitest.arch.config.ts | 3 + apps/ui-staff/mock-oidc.users.json | 2 +- apps/ui-staff/package.json | 2 + .../archunit-tests/ui-app-composition.test.ts | 6 + apps/ui-staff/vitest.arch.config.ts | 3 + packages/cellix/archunit-tests/manifest.md | 54 +++++ packages/cellix/archunit-tests/package.json | 12 ++ packages/cellix/archunit-tests/readme.md | 43 ++++ packages/cellix/archunit-tests/src/api.ts | 3 + .../application-composition.contract.test.ts | 99 ++++++++++ .../src/checks/api-composition.ts | 94 +++++++++ .../src/checks/serenity-suite-conventions.ts | 185 ++++++++++++++++++ .../src/checks/ui-app-composition.ts | 70 +++++++ .../architecture-stubs/api/src/cellix.ts | 6 + .../architecture-stubs/api/src/index.ts | 3 + .../step-definitions/community.steps.ts | 3 + .../community/step-definitions/index.ts | 1 + .../serenity/src/cucumber-lifecycle-hooks.ts | 1 + .../serenity/src/infrastructure.ts | 3 + .../serenity/src/step-definitions/index.ts | 1 + .../architecture-stubs/serenity/src/world.ts | 1 + .../architecture-stubs/ui/src/App.tsx | 3 + .../architecture-stubs/ui/src/main.tsx | 1 + packages/cellix/archunit-tests/src/index.ts | 3 + .../src/serenity.contract.test.ts | 82 ++++++++ .../cellix/archunit-tests/src/serenity.ts | 3 + .../src/stub-rejection.contract.test.ts | 34 ++++ .../src/test-suites/api-composition.ts | 16 ++ .../test-suites/serenity-suite-conventions.ts | 22 +++ .../src/test-suites/ui-app-composition.ts | 16 ++ packages/cellix/archunit-tests/src/ui-app.ts | 3 + packages/cellix/archunit-tests/tsconfig.json | 2 +- .../src/settings/timeout-settings.ts | 9 + .../tests/file-user-store.test.ts | 14 +- .../tests/portal-discovery.test.ts | 6 +- .../acceptance-api/package.json | 6 +- .../serenity-conventions.test.ts | 6 + .../src/contexts/staff/notes/staff-notes.ts | 3 + .../staff/questions/staff-target-route.ts | 4 + .../step-definitions/staff-landing.steps.ts | 13 +- .../staff/tasks/open-staff-landing.ts | 7 + .../acceptance-api/vitest.arch.config.ts | 3 + .../acceptance-ui/package.json | 6 +- .../serenity-conventions.test.ts | 5 + .../acceptance-ui/vitest.arch.config.ts | 3 + .../ocom-verification/e2e-tests/package.json | 6 +- .../serenity-conventions.test.ts | 6 + .../e2e-tests/src/cucumber-lifecycle-hooks.ts | 3 + .../shared/environment/test-environment.ts | 46 +++++ .../e2e-tests/vitest.arch.config.ts | 3 + .../ocom/service-queue-storage/turbo.json | 2 +- pnpm-lock.yaml | 74 ++++--- pnpm-workspace.yaml | 7 +- 59 files changed, 974 insertions(+), 63 deletions(-) create mode 100644 apps/api/src/archunit-tests/api-composition.test.ts create mode 100644 apps/ui-community/src/archunit-tests/ui-app-composition.test.ts create mode 100644 apps/ui-community/vitest.arch.config.ts create mode 100644 apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts create mode 100644 apps/ui-staff/vitest.arch.config.ts create mode 100644 packages/cellix/archunit-tests/manifest.md create mode 100644 packages/cellix/archunit-tests/src/api.ts create mode 100644 packages/cellix/archunit-tests/src/application-composition.contract.test.ts create mode 100644 packages/cellix/archunit-tests/src/checks/api-composition.ts create mode 100644 packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts create mode 100644 packages/cellix/archunit-tests/src/checks/ui-app-composition.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/cellix.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/api/src/index.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/community.steps.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx create mode 100644 packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx create mode 100644 packages/cellix/archunit-tests/src/serenity.contract.test.ts create mode 100644 packages/cellix/archunit-tests/src/serenity.ts create mode 100644 packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts create mode 100644 packages/cellix/archunit-tests/src/test-suites/api-composition.ts create mode 100644 packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts create mode 100644 packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts create mode 100644 packages/cellix/archunit-tests/src/ui-app.ts create mode 100644 packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts create mode 100644 packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts create mode 100644 packages/ocom-verification/acceptance-api/vitest.arch.config.ts create mode 100644 packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts create mode 100644 packages/ocom-verification/acceptance-ui/vitest.arch.config.ts create mode 100644 packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts create mode 100644 packages/ocom-verification/e2e-tests/vitest.arch.config.ts diff --git a/.snyk b/.snyk index 78207903f..3ee1eb65b 100644 --- a/.snyk +++ b/.snyk @@ -1,5 +1,10 @@ version: v1.5.0 ignore: + 'SNYK-JS-BRACEEXPANSION-17706650': + - '* > serve-handler@6.1.7 > minimatch@3.1.5 > brace-expansion': + reason: 'ReDoS is fixed only in brace-expansion@5.0.7, whose CJS build uses a named export (exports.expand). Docusaurus'' serve-handler pins minimatch@3.1.5, which does `var expand = require(''brace-expansion'')` and calls it directly, so 5.0.7 throws "expand is not a function". minimatch 3.x cannot be upgraded (serve-handler needs its function-default API). Every other project in the repo is on the fixed brace-expansion@5.0.7 via overrides; this path is docs-only, build-time, over repository-controlled input, and not exploitable.' + expires: '2026-10-01T00:00:00.000Z' + created: '2026-07-01T00:00:00.000Z' 'SNYK-JS-SIRV-12558119': - '* > sirv@2.0.4': reason: 'Transitive dependency in Docusaurus; not exploitable in static site serving context (dev-only asset handler)' diff --git a/apps/api/package.json b/apps/api/package.json index 6467e1ff0..ff78c4cb7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -47,6 +47,7 @@ "@opentelemetry/api": "1.9.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-rolldown": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", diff --git a/apps/api/src/archunit-tests/api-composition.test.ts b/apps/api/src/archunit-tests/api-composition.test.ts new file mode 100644 index 000000000..6f1ef5980 --- /dev/null +++ b/apps/api/src/archunit-tests/api-composition.test.ts @@ -0,0 +1,5 @@ +import { describeApiCompositionTests } from '@cellix/archunit-tests/api'; + +describeApiCompositionTests({ + apiIndexPath: 'src/index.ts', +}); diff --git a/apps/ui-community/mock-oidc.users.json b/apps/ui-community/mock-oidc.users.json index ade0b4f42..f5518cba9 100644 --- a/apps/ui-community/mock-oidc.users.json +++ b/apps/ui-community/mock-oidc.users.json @@ -3,7 +3,7 @@ "username": "test@example.com", "sub": "00000000-0000-4000-8000-000000000001", "password": "password", - "oidcConfigName": "end-user", + "oidcConfigName": "end-user", "claims": { "email": "test@example.com", "given_name": "Test", @@ -15,7 +15,7 @@ "username": "john.doe@example.com", "sub": "00000000-0000-4000-8000-000000000002", "password": "password", - "oidcConfigName": "end-user", + "oidcConfigName": "end-user", "claims": { "email": "john.doe@example.com", "given_name": "John", @@ -27,7 +27,7 @@ "username": "owner@test.example", "sub": "aaaaaaaa-bbbb-1ccc-9ddd-eeeeeeeeee01", "password": "password", - "oidcConfigName": "end-user", + "oidcConfigName": "end-user", "claims": { "email": "owner@test.example", "given_name": "Test", diff --git a/apps/ui-community/package.json b/apps/ui-community/package.json index 7d3f4dfac..f3f516458 100644 --- a/apps/ui-community/package.json +++ b/apps/ui-community/package.json @@ -14,6 +14,7 @@ "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", + "test:arch": "vitest run --config vitest.arch.config.ts", "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest", "storybook": "storybook dev -p 6008", @@ -37,6 +38,7 @@ "react-router-dom": "catalog:" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@chromatic-com/storybook": "^5.2.1", diff --git a/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts b/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts new file mode 100644 index 000000000..6c4afc0eb --- /dev/null +++ b/apps/ui-community/src/archunit-tests/ui-app-composition.test.ts @@ -0,0 +1,6 @@ +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['React.StrictMode', 'ThemeProvider', 'ConfigProvider', 'AntdApp', 'HelmetProvider', 'BrowserRouter', 'AuthProvider'], +}); diff --git a/apps/ui-community/vitest.arch.config.ts b/apps/ui-community/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/apps/ui-community/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/apps/ui-staff/mock-oidc.users.json b/apps/ui-staff/mock-oidc.users.json index 6b635dfb4..1c753ea7c 100644 --- a/apps/ui-staff/mock-oidc.users.json +++ b/apps/ui-staff/mock-oidc.users.json @@ -3,7 +3,7 @@ "username": "staff@ownercommunity.onmicrosoft.com", "sub": "10000000-0000-4000-8000-000000000001", "password": "password", - "oidcConfigName": "staff-user", + "oidcConfigName": "staff-user", "claims": { "email": "staff@ownercommunity.onmicrosoft.com", "given_name": "Staff", diff --git a/apps/ui-staff/package.json b/apps/ui-staff/package.json index f5c74fc0b..465156724 100644 --- a/apps/ui-staff/package.json +++ b/apps/ui-staff/package.json @@ -14,6 +14,7 @@ "start": "vite", "preview": "vite preview", "test": "vitest run --silent --reporter=dot", + "test:arch": "vitest run --config vitest.arch.config.ts", "test:coverage": "vitest run --coverage --silent --reporter=dot", "test:watch": "vitest" }, @@ -38,6 +39,7 @@ "react-router-dom": "catalog:" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", "@cellix/config-vitest": "workspace:*", "@types/react": "^19.1.8", diff --git a/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts b/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts new file mode 100644 index 000000000..6c4afc0eb --- /dev/null +++ b/apps/ui-staff/src/archunit-tests/ui-app-composition.test.ts @@ -0,0 +1,6 @@ +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['React.StrictMode', 'ThemeProvider', 'ConfigProvider', 'AntdApp', 'HelmetProvider', 'BrowserRouter', 'AuthProvider'], +}); diff --git a/apps/ui-staff/vitest.arch.config.ts b/apps/ui-staff/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/apps/ui-staff/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/cellix/archunit-tests/manifest.md b/packages/cellix/archunit-tests/manifest.md new file mode 100644 index 000000000..3a4feae23 --- /dev/null +++ b/packages/cellix/archunit-tests/manifest.md @@ -0,0 +1,54 @@ +# @cellix/archunit-tests package manifest + +## Purpose + +Provide reusable architectural fitness checks that Cellix applications and libraries can register in their own Vitest suites. The package turns framework composition and organization conventions into executable, consumer-owned contracts. + +## Scope + +The package inspects TypeScript project source and registers Vitest architecture suites. It covers general framework composition, infrastructure initialization, package-layer conventions, and configurable source organization. + +## Public API shape + +Each focused export module exposes configuration types, asynchronous checkers that return all discovered violations, and `describe...Tests` helpers that register the checker with Vitest. + +## Core concepts + +- A checker reports violations without loading consumer runtime modules. +- A suite helper lets each consumer own and run its architecture contract. +- Configuration makes project paths and providers explicit, while Serenity bounded contexts are discovered from the consumer's standard context directory instead of embedding OCom-specific names in reusable rules. + +## Intended consumers + +- Cellix-based API and UI application entrypoints +- Cellix-based Serenity/Cucumber acceptance and end-to-end suites +- Cellix library packages that opt into the existing domain, persistence, GraphQL, application-service, frontend, or general conventions + +## Package boundaries + +- Public modules expose configuration types, violation-returning checkers, and Vitest suite registration helpers. +- Checks inspect consumer-owned source trees and do not import or execute consumer runtime code. +- Application-specific paths and bounded-context names are supplied by consumers. + +## Non-goals + +- Owning OCom-specific business rules or package names +- Generating application code +- Replacing behavioral, acceptance, or end-to-end tests +- Enforcing one fixed monorepo topology where a configurable project path is sufficient + +## Dependencies / relationships + +The package depends on Node filesystem APIs, Vitest for suite registration, and ArchUnit for established dependency checks. Consumer packages depend on a focused package subpath and provide only their own source paths and conventions. + +## Testing strategy + +Public contract tests exercise success and violation cases through module entrypoints. Real API, UI, acceptance, and E2E packages then register the reusable suites as downstream architecture tests. + +## Documentation obligations + +Every new public module must be represented in this manifest and the consumer README. Public functions require parameter, return, and usage documentation at their declaration. + +## Release-readiness standards + +The package build, full package test suite, and representative downstream consumer suites must pass. Additive exports must remain narrowly scoped; removals or behavior changes require downstream impact review and explicit migration guidance. diff --git a/packages/cellix/archunit-tests/package.json b/packages/cellix/archunit-tests/package.json index 96dcca505..ff57c093c 100644 --- a/packages/cellix/archunit-tests/package.json +++ b/packages/cellix/archunit-tests/package.json @@ -39,6 +39,18 @@ "./general": { "types": "./dist/src/general.d.ts", "default": "./dist/src/general.js" + }, + "./api": { + "types": "./dist/src/api.d.ts", + "default": "./dist/src/api.js" + }, + "./ui-app": { + "types": "./dist/src/ui-app.d.ts", + "default": "./dist/src/ui-app.js" + }, + "./serenity": { + "types": "./dist/src/serenity.d.ts", + "default": "./dist/src/serenity.js" } }, "scripts": { diff --git a/packages/cellix/archunit-tests/readme.md b/packages/cellix/archunit-tests/readme.md index 45619c042..712478b74 100644 --- a/packages/cellix/archunit-tests/readme.md +++ b/packages/cellix/archunit-tests/readme.md @@ -15,6 +15,49 @@ Current rules in this package include: - TypeScript compiler output conventions for `packages/cellix/*` - circular dependency checks across Cellix package source graphs - framework boundary checks, such as `ui-core` not depending on OCom UI code or app code +- API startup composition through infrastructure, context, application services, handlers, and startup, including service initialization during Azure `appStart` +- UI application bootstrap composition, including root validation, configured providers, and React Router +- Serenity/Cucumber managed-world initialization, concrete finalized server infrastructure, lifecycle cleanup, context loaders, and Screenplay delegation + +## Application Composition + +Register the API rule from the API package itself: + +```typescript +import { describeApiCompositionTests } from '@cellix/archunit-tests/api'; + +describeApiCompositionTests({ apiIndexPath: 'src/index.ts' }); +``` + +UI applications configure the providers their bootstrap is expected to compose: + +```typescript +import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; + +describeUiAppCompositionTests({ + appRoot: 'src', + requiredProviders: ['ThemeProvider', 'BrowserRouter', 'AuthProvider'], +}); +``` + +## Serenity Suites + +Each acceptance or E2E package points the convention at its source root. Contexts are discovered automatically from the immediate directories under `src/contexts`. Infrastructure-backed suites can additionally require scenario cleanup and suite shutdown: + +```typescript +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); +``` + +The Serenity convention requires a managed world and lifecycle, a central context loader, context-local `*.steps.ts(x)` files, and steps that delegate through context-local Tasks, Interactions, or Questions. Suites that require managed cleanup must also export finalized `ApiInfrastructure` or `E2EInfrastructure` with at least one server; E2E infrastructure must include a UI portal. + +## Verifying the Architecture Tests + +The package includes intentionally incomplete portable projects under `src/fixtures/architecture-stubs/`. The package's normal architecture suite points each checker at its stub and asserts that the expected violations are reported. These test-of-the-tests run automatically through `test:arch`; no separate stub command or test area is required. ## What It Does Not Own diff --git a/packages/cellix/archunit-tests/src/api.ts b/packages/cellix/archunit-tests/src/api.ts new file mode 100644 index 000000000..169fa848b --- /dev/null +++ b/packages/cellix/archunit-tests/src/api.ts @@ -0,0 +1,3 @@ +export type { ApiCompositionConfig } from './checks/api-composition.js'; +export { checkApiComposition } from './checks/api-composition.js'; +export { describeApiCompositionTests } from './test-suites/api-composition.js'; diff --git a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts new file mode 100644 index 000000000..d7cf0878a --- /dev/null +++ b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts @@ -0,0 +1,99 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { checkApiComposition } from './api.js'; +import { checkUiAppComposition } from './ui-app.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +async function fixture(fileName: string, content: string): Promise { + const directory = await mkdtemp(path.join(tmpdir(), 'cellix-archunit-')); + temporaryDirectories.push(directory); + const filePath = path.join(directory, fileName); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); + return filePath; +} + +describe('application composition public contracts', () => { + it('accepts a complete Cellix API startup chain', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service)) + .setContext((registry) => { return { registry }; }) + .initializeApplicationServices((context) => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { + private setupLifecycle() { + app.hook.appStart(async () => { + await this.startAllServicesWithTracing(); + this.contextInternal = this.contextCreatorInternal(this); + this.appServicesHostInternal = this.appServicesHostBuilder(this.contextInternal); + }); + } + private async startAllServicesWithTracing() { + await Promise.all(this.services.map((service) => service.startUp())); + } +}`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toStrictEqual([]); + }); + + it('reports omitted and out-of-order API startup stages', async () => { + const apiIndexPath = await fixture('index.ts', `import { Cellix } from './cellix.ts';\nCellix.startUp().setContext(() => ({}));`); + + const violations = await checkApiComposition({ apiIndexPath }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('initializeInfrastructureServices'), expect.stringContaining('initializeApplicationServices'), expect.stringContaining('registerAzureFunction')])); + }); + + it('reports a Cellix implementation that does not initialize infrastructure during app startup', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service)) + .setContext((registry) => { return { registry }; }) + .initializeApplicationServices((context) => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile(path.join(path.dirname(apiIndexPath), 'cellix.ts'), `export class Cellix { private setupLifecycle() { app.hook.appStart(async () => this.buildContext()); } }`); + + expect(await checkApiComposition({ apiIndexPath })).toEqual(expect.arrayContaining([expect.stringContaining('registered infrastructure services during appStart')])); + }); + + it('accepts a UI bootstrap with the configured providers and routed app', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.tsx'; +const root = document.getElementById('root'); +if (!root) throw new Error('missing root'); +createRoot(root).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider', 'BrowserRouter'] })).toStrictEqual([]); + }); + + it('reports a UI entrypoint that skips root validation, providers, or routing', async () => { + const appRoot = path.dirname(await fixture('src/main.tsx', `import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render();`)); + await writeFile(path.join(appRoot, 'App.tsx'), 'export default () =>
;'); + + const violations = await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider'] }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('guard'), expect.stringContaining('ThemeProvider'), expect.stringContaining('Routes')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/checks/api-composition.ts b/packages/cellix/archunit-tests/src/checks/api-composition.ts new file mode 100644 index 000000000..22f2b24c2 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/api-composition.ts @@ -0,0 +1,94 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +export interface ApiCompositionConfig { + /** Path to the API composition root, normally `src/index.ts`. */ + apiIndexPath: string; +} + +const requiredStages = ['initializeInfrastructureServices', 'setContext', 'initializeApplicationServices', 'registerAzureFunction', 'startUp'] as const; + +/** + * Check that an API entry point composes every Cellix runtime layer in startup order. + * + * @param config - Location of the consumer's API composition entrypoint. + * @returns Every composition violation; an empty array means the entrypoint complies. + * @example + * ```typescript + * await checkApiComposition({ apiIndexPath: 'src/index.ts' }); + * ``` + */ +export async function checkApiComposition(config: ApiCompositionConfig): Promise { + if (!config.apiIndexPath) { + throw new Error('checkApiComposition requires apiIndexPath to be set'); + } + + let content: string; + try { + content = await readFile(config.apiIndexPath, 'utf8'); + } catch { + return [`[${config.apiIndexPath}] API composition root must exist`]; + } + + const violations: string[] = []; + if (!/import\s+\{[^}]*\bCellix\b[^}]*\}\s+from\s+['"][^'"]+['"]/.test(content)) { + violations.push(`[${config.apiIndexPath}] API composition root must import Cellix`); + } + + const positions = requiredStages.map((stage) => ({ stage, position: content.indexOf(`.${stage}`) })); + for (const { stage, position } of positions) { + if (position < 0) { + violations.push(`[${config.apiIndexPath}] API composition root must call ${stage}${stage === 'registerAzureFunction' ? '* for at least one handler' : ''}`); + } + } + + const presentPositions = positions.filter(({ position }) => position >= 0); + for (let index = 1; index < presentPositions.length; index += 1) { + const previous = presentPositions[index - 1]; + const current = presentPositions[index]; + if (previous && current && current.position < previous.position) { + violations.push(`[${config.apiIndexPath}] ${current.stage} must be composed after ${previous.stage}`); + } + } + + if (!/initializeInfrastructureServices(?:<[^;]+?>)?\s*\(\s*\(?\s*\w+\s*\)?\s*=>[\s\S]*?registerInfrastructureService/.test(content)) { + violations.push(`[${config.apiIndexPath}] initializeInfrastructureServices must register at least one infrastructure service`); + } + if (!/\.setContext\s*\(\s*\(?\s*\w+\s*\)?\s*=>[\s\S]*?return\s*\{/.test(content)) { + violations.push(`[${config.apiIndexPath}] setContext must build and return the runtime context`); + } + + await checkCellixInfrastructureLifecycle(config.apiIndexPath, content, violations); + + return violations; +} + +async function checkCellixInfrastructureLifecycle(apiIndexPath: string, apiIndex: string, violations: string[]): Promise { + const cellixImport = apiIndex.match(/import\s+\{[^}]*\bCellix\b[^}]*\}\s+from\s+['"](\.\/[^'"]+)['"]/); + const importPath = cellixImport?.[1]; + if (!importPath) { + return; + } + + const cellixPath = path.resolve(path.dirname(apiIndexPath), importPath); + let cellix: string; + try { + cellix = await readFile(cellixPath, 'utf8'); + } catch { + violations.push(`[${cellixPath}] Cellix infrastructure lifecycle implementation must exist`); + return; + } + + const appStartPosition = cellix.indexOf('app.hook.appStart'); + const serviceStartupPosition = cellix.indexOf('startAllServicesWithTracing', appStartPosition); + const contextCreationPosition = cellix.indexOf('contextCreatorInternal', appStartPosition); + if (appStartPosition < 0 || serviceStartupPosition < 0) { + violations.push(`[${cellixPath}] Cellix must initialize registered infrastructure services during appStart`); + return; + } + if (contextCreationPosition < 0) { + violations.push(`[${cellixPath}] Cellix appStart must create the runtime context after infrastructure initialization`); + } else if (serviceStartupPosition > contextCreationPosition) { + violations.push(`[${cellixPath}] Cellix must initialize infrastructure services before creating the runtime context`); + } +} diff --git a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts new file mode 100644 index 000000000..25a9080b1 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts @@ -0,0 +1,185 @@ +import { access, readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; + +export interface SerenitySuiteConventionsConfig { + /** Source root containing the suite world, lifecycle hooks, and contexts. */ + suiteRoot: string; + /** Require per-scenario cleanup and suite-level infrastructure shutdown. */ + requireManagedCleanup?: boolean; +} + +async function exists(filePath: string): Promise { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Check the managed-world and Screenplay organization contract of a Serenity suite. + * + * @param config - Suite source root and cleanup policy. Context directories are discovered automatically. + * @returns Every initialization or organization violation; an empty array means the suite complies. + * @example + * ```typescript + * await checkSerenitySuiteConventions({ suiteRoot: 'src' }); + * ``` + */ +export async function checkSerenitySuiteConventions(config: SerenitySuiteConventionsConfig): Promise { + if (!config.suiteRoot) { + throw new Error('checkSerenitySuiteConventions requires suiteRoot to be set'); + } + const violations: string[] = []; + const worldPath = path.join(config.suiteRoot, 'world.ts'); + const lifecyclePath = path.join(config.suiteRoot, 'cucumber-lifecycle-hooks.ts'); + const loaderPath = path.join(config.suiteRoot, 'step-definitions', 'index.ts'); + const contextsRoot = path.join(config.suiteRoot, 'contexts'); + + for (const requiredPath of [worldPath, lifecyclePath, loaderPath]) { + if (!(await exists(requiredPath))) { + violations.push(`[${requiredPath}] Required Serenity suite file must exist`); + } + } + + if (await exists(worldPath)) { + const world = await readFile(worldPath, 'utf8'); + if (!/import\s+\{[^}]*registerManagedSerenityWorld[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/cucumber['"]/.test(world)) { + violations.push(`[${worldPath}] Managed world must use registerManagedSerenityWorld from @cellix/serenity-framework/cucumber`); + } + if (!/import\s+\{[^}]*SerenityCast[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/serenity['"]/.test(world)) { + violations.push(`[${worldPath}] Managed world must use SerenityCast from @cellix/serenity-framework/serenity`); + } + for (const required of ['registerManagedSerenityWorld', 'new SerenityCast', 'createCast:', 'useNotepad: true', 'registerLifecycleHooks()']) { + if (!world.includes(required)) { + violations.push(`[${worldPath}] Managed world must include ${required}`); + } + } + if (!/export\s+const\s+\w+World\s*=/.test(world) || !/export\s+type\s+\w+World\s*=\s*InstanceType/.test(world)) { + violations.push(`[${worldPath}] Managed world must export both its constructor and InstanceType`); + } + } + + if (await exists(lifecyclePath)) { + const lifecycle = await readFile(lifecyclePath, 'utf8'); + if (!/import\s+\{[^}]*registerWorldLifecycleHooks[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/cucumber['"]/.test(lifecycle)) { + violations.push(`[${lifecyclePath}] Lifecycle must use registerWorldLifecycleHooks from @cellix/serenity-framework/cucumber`); + } + if (!/import\s+\{[^}]*getTimeout[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/settings['"]/.test(lifecycle)) { + violations.push(`[${lifecyclePath}] Lifecycle must use shared timeout settings`); + } + for (const required of ['registerWorldLifecycleHooks', "getTimeout('scenario')", 'world.init()']) { + if (!lifecycle.includes(required)) { + violations.push(`[${lifecyclePath}] Lifecycle hooks must include ${required}`); + } + } + if (config.requireManagedCleanup && !lifecycle.includes('world.cleanup()')) { + violations.push(`[${lifecyclePath}] Managed infrastructure suites must clean up the world after each scenario`); + } + if (config.requireManagedCleanup && !/afterAll:\s*\(\)\s*=>\s*infrastructure\.stopAll\(\)/.test(lifecycle)) { + violations.push(`[${lifecyclePath}] Managed infrastructure suites must stop all infrastructure after the suite`); + } + } + if (config.requireManagedCleanup) { + await checkManagedInfrastructure(config.suiteRoot, violations); + } + + const loader = (await exists(loaderPath)) ? await readFile(loaderPath, 'utf8') : ''; + const contexts = await discoverContexts(contextsRoot); + if (contexts.length === 0) { + violations.push(`[${contextsRoot}] Serenity suite must define at least one context directory`); + } + for (const context of contexts) { + await checkContext(config.suiteRoot, context, loaderPath, loader, violations); + } + + return violations; +} + +async function checkManagedInfrastructure(suiteRoot: string, violations: string[]): Promise { + const infrastructurePath = path.join(suiteRoot, 'infrastructure.ts'); + if (!(await exists(infrastructurePath))) { + violations.push(`[${infrastructurePath}] Managed Serenity suites must define their infrastructure`); + return; + } + + const infrastructure = await readFile(infrastructurePath, 'utf8'); + const frameworkImport = infrastructure.match(/import\s+\{\s*(E2EInfrastructure|ApiInfrastructure)\s*\}\s+from\s+['"]@cellix\/serenity-framework\/infrastructure\/(e2e|api)['"]/); + const infrastructureKind = frameworkImport?.[1]; + if (!infrastructureKind) { + violations.push(`[${infrastructurePath}] Managed infrastructure must use a Cellix Serenity infrastructure builder`); + return; + } + if (!new RegExp(`export\\s+const\\s+infrastructure\\s*=\\s*${infrastructureKind}\\.create\\s*\\(`).test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure must export a created ${infrastructureKind}`); + } + if (!/\.addServer\s*\(/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure must register at least one server`); + } + if (infrastructureKind === 'E2EInfrastructure' && !/\.addUiPortal\s*\(/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] E2E infrastructure must register at least one UI portal`); + } + if (!/\.finalize\s*\(\s*\)/.test(infrastructure)) { + violations.push(`[${infrastructurePath}] Managed infrastructure registration must be finalized`); + } +} + +async function discoverContexts(contextsRoot: string): Promise { + try { + const entries = await readdir(contextsRoot, { withFileTypes: true }); + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); + } catch { + return []; + } +} + +async function checkContext(suiteRoot: string, context: string, loaderPath: string, loader: string, violations: string[]): Promise { + const contextRoot = path.join(suiteRoot, 'contexts', context); + const contextIndex = path.join(contextRoot, 'step-definitions', 'index.ts'); + if (!loader.includes(`../contexts/${context}/step-definitions/index.ts`)) { + violations.push(`[${loaderPath}] Central loader must import the ${context} context step-definition index`); + } + if (!(await exists(contextIndex))) { + violations.push(`[${contextIndex}] Context step-definition index must exist`); + return; + } + + const stepDirectory = path.dirname(contextIndex); + const stepFiles = (await readdir(stepDirectory)).filter((name) => /\.steps\.tsx?$/.test(name)); + if (stepFiles.length === 0) { + violations.push(`[${stepDirectory}] Context must contain at least one *.steps.ts or *.steps.tsx file`); + } + + const indexContent = await readFile(contextIndex, 'utf8'); + for (const stepFile of stepFiles) { + if (!indexContent.includes(`./${stepFile}`)) { + violations.push(`[${contextIndex}] Context index must import ${stepFile}`); + } + const stepPath = path.join(stepDirectory, stepFile); + const stepContent = await readFile(stepPath, 'utf8'); + if (!/from\s+['"]@cucumber\/cucumber['"]/.test(stepContent)) { + violations.push(`[${stepPath}] Step definitions must use Cucumber`); + } + const abstractionImports = [...stepContent.matchAll(/from\s+['"]\.\.\/(tasks|questions)\/([^'"]+)['"]/g)]; + if (abstractionImports.length === 0 || !/\.(attemptsTo|answer)\s*\(/.test(stepContent)) { + violations.push(`[${stepPath}] Step definitions must delegate behavior to a task, interaction, or question`); + } + for (const match of abstractionImports) { + const relativeFile = match[2]; + if (!relativeFile) continue; + const abstractionPath = path.join(contextRoot, match[1] as string, relativeFile); + if (!(await exists(abstractionPath))) { + violations.push(`[${abstractionPath}] Imported Screenplay abstraction must exist`); + continue; + } + const abstraction = await readFile(abstractionPath, 'utf8'); + if (!/from\s+['"]@serenity-js\/core['"]/.test(abstraction) || !/\b(Task|Interaction|Question)\b/.test(abstraction)) { + violations.push(`[${abstractionPath}] Context tasks and questions must implement a Serenity Screenplay abstraction`); + } + } + } +} diff --git a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts new file mode 100644 index 000000000..50d5e6110 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts @@ -0,0 +1,70 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +export interface UiAppCompositionConfig { + /** Directory containing `main.tsx` and `App.tsx`. */ + appRoot: string; + /** Provider component names that must wrap the application bootstrap. */ + requiredProviders?: string[]; +} + +async function readSource(filePath: string, violations: string[]): Promise { + try { + return await readFile(filePath, 'utf8'); + } catch { + violations.push(`[${filePath}] Required UI composition file must exist`); + return undefined; + } +} + +/** + * Check that a Cellix browser app validates its mount point and composes its configured providers and routes. + * + * @param config - Source root and provider names required by the consumer application. + * @returns Every bootstrap violation; an empty array means the UI composition complies. + * @example + * ```typescript + * await checkUiAppComposition({ appRoot: 'src', requiredProviders: ['BrowserRouter'] }); + * ``` + */ +export async function checkUiAppComposition(config: UiAppCompositionConfig): Promise { + if (!config.appRoot) { + throw new Error('checkUiAppComposition requires appRoot to be set'); + } + + const violations: string[] = []; + const mainPath = path.join(config.appRoot, 'main.tsx'); + const appPath = path.join(config.appRoot, 'App.tsx'); + const main = await readSource(mainPath, violations); + const app = await readSource(appPath, violations); + + if (main) { + if (!/createRoot\s*\(/.test(main)) { + violations.push(`[${mainPath}] UI bootstrap must create a React root`); + } + if (!/getElementById\s*\(\s*['"]root['"]\s*\)/.test(main)) { + violations.push(`[${mainPath}] UI bootstrap must resolve the #root mount element`); + } + if (!/if\s*\(\s*!\s*\w+\s*\)[\s\S]*?throw\s+new\s+Error/.test(main)) { + violations.push(`[${mainPath}] UI bootstrap must guard against a missing root element`); + } + if (!//.test(main)) { + violations.push(`[${mainPath}] UI bootstrap must render App`); + } + for (const provider of config.requiredProviders ?? []) { + const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (!new RegExp(`<${escapedProvider}\\b`).test(main)) { + violations.push(`[${mainPath}] UI bootstrap must compose ${provider}`); + } + } + } + + if (app && (!/from\s+['"]react-router-dom['"]/.test(app) || !/ fetch('/api')); diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/contexts/community/step-definitions/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/cucumber-lifecycle-hooks.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts new file mode 100644 index 000000000..de7908ea8 --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/infrastructure.ts @@ -0,0 +1,3 @@ +import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; + +export const infrastructure = E2EInfrastructure.create().finalize(); diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/step-definitions/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/serenity/src/world.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx new file mode 100644 index 000000000..9d6e50f4b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/App.tsx @@ -0,0 +1,3 @@ +export default function App() { + return null; +} diff --git a/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/cellix/archunit-tests/src/fixtures/architecture-stubs/ui/src/main.tsx @@ -0,0 +1 @@ +export {}; diff --git a/packages/cellix/archunit-tests/src/index.ts b/packages/cellix/archunit-tests/src/index.ts index 1385a951c..4e6b32aa5 100644 --- a/packages/cellix/archunit-tests/src/index.ts +++ b/packages/cellix/archunit-tests/src/index.ts @@ -1,3 +1,4 @@ +export * from './api.js'; export * from './application-services.js'; export * from './data-sources-mongoose-models.js'; export * from './domain.js'; @@ -5,3 +6,5 @@ export * from './frontend.js'; export * from './general.js'; export * from './graphql.js'; export * from './persistence.js'; +export * from './serenity.js'; +export * from './ui-app.js'; diff --git a/packages/cellix/archunit-tests/src/serenity.contract.test.ts b/packages/cellix/archunit-tests/src/serenity.contract.test.ts new file mode 100644 index 000000000..1a023bc1f --- /dev/null +++ b/packages/cellix/archunit-tests/src/serenity.contract.test.ts @@ -0,0 +1,82 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { checkSerenitySuiteConventions } from './serenity.js'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true }))); +}); + +async function write(root: string, relativePath: string, content: string): Promise { + const filePath = path.join(root, relativePath); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, content); +} + +async function suiteFixture(): Promise { + const suiteRoot = await mkdtemp(path.join(tmpdir(), 'cellix-serenity-')); + temporaryDirectories.push(suiteRoot); + await write( + suiteRoot, + 'world.ts', + `import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber';\nimport { SerenityCast } from '@cellix/serenity-framework/serenity';\nimport { infrastructure } from './infrastructure.ts';\nexport const TestWorld = registerManagedSerenityWorld({ infrastructure, createCast: () => new SerenityCast({ useNotepad: true }) });\nexport type TestWorld = InstanceType;\nregisterLifecycleHooks();`, + ); + await write( + suiteRoot, + 'cucumber-lifecycle-hooks.ts', + `import { registerWorldLifecycleHooks } from '@cellix/serenity-framework/cucumber';\nimport { getTimeout } from '@cellix/serenity-framework/settings';\nregisterWorldLifecycleHooks({ scenarioTimeout: getTimeout('scenario'), before: async (world) => world.init(), after: async (world) => world.cleanup(), afterAll: () => infrastructure.stopAll() });`, + ); + await write(suiteRoot, 'step-definitions/index.ts', `import '../contexts/community/step-definitions/index.ts';`); + await write(suiteRoot, 'contexts/community/step-definitions/index.ts', `import './create-community.steps.ts';`); + await write( + suiteRoot, + 'contexts/community/step-definitions/create-community.steps.ts', + `import { Given, When, Then } from '@cucumber/cucumber';\nimport { CreateCommunity } from '../tasks/create-community.ts';\nimport { CommunityName } from '../questions/community-name.ts';\nWhen('creates', async () => actor.attemptsTo(CreateCommunity()));\nThen('created', async () => actor.answer(CommunityName()));`, + ); + await write(suiteRoot, 'contexts/community/tasks/create-community.ts', `import { Interaction } from '@serenity-js/core';\nexport const CreateCommunity = () => Interaction.where('creates', async () => undefined);`); + await write(suiteRoot, 'contexts/community/questions/community-name.ts', `import { Question } from '@serenity-js/core';\nexport const CommunityName = () => Question.about('name', () => 'Cellix');`); + await write( + suiteRoot, + 'infrastructure.ts', + `import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e'; +export const infrastructure = E2EInfrastructure.create() + .addServer('api', apiServer) + .addUiPortal('community', communityPortal, { dependsOn: ['api'] }) + .finalize();`, + ); + return suiteRoot; +} + +describe('Serenity convention public contract', () => { + it('accepts managed lifecycle and context-owned Screenplay abstractions', async () => { + const suiteRoot = await suiteFixture(); + expect(await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true })).toStrictEqual([]); + }); + + it('reports missing context indexes and steps that contain behavior directly', async () => { + const suiteRoot = await suiteFixture(); + await write(suiteRoot, 'step-definitions/index.ts', ''); + await write(suiteRoot, 'contexts/community/step-definitions/create-community.steps.ts', `import { When } from '@cucumber/cucumber';\nWhen('creates', async () => fetch('/graphql'));`); + + const violations = await checkSerenitySuiteConventions({ suiteRoot }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('Central loader'), expect.stringContaining('delegate behavior')])); + }); + + it('reports a suite with no discoverable contexts', async () => { + const suiteRoot = await suiteFixture(); + await rm(path.join(suiteRoot, 'contexts'), { recursive: true }); + + expect(await checkSerenitySuiteConventions({ suiteRoot })).toContain(`[${path.join(suiteRoot, 'contexts')}] Serenity suite must define at least one context directory`); + }); + + it('reports managed infrastructure without concrete server and UI portal registration', async () => { + const suiteRoot = await suiteFixture(); + await write(suiteRoot, 'infrastructure.ts', `import { E2EInfrastructure } from '@cellix/serenity-framework/infrastructure/e2e';\nexport const infrastructure = E2EInfrastructure.create().finalize();`); + + const violations = await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true }); + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('at least one server'), expect.stringContaining('at least one UI portal')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/serenity.ts b/packages/cellix/archunit-tests/src/serenity.ts new file mode 100644 index 000000000..bbd779e01 --- /dev/null +++ b/packages/cellix/archunit-tests/src/serenity.ts @@ -0,0 +1,3 @@ +export type { SerenitySuiteConventionsConfig } from './checks/serenity-suite-conventions.js'; +export { checkSerenitySuiteConventions } from './checks/serenity-suite-conventions.js'; +export { describeSerenitySuiteConventionTests } from './test-suites/serenity-suite-conventions.js'; diff --git a/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts b/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts new file mode 100644 index 000000000..0490e6ac3 --- /dev/null +++ b/packages/cellix/archunit-tests/src/stub-rejection.contract.test.ts @@ -0,0 +1,34 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { checkApiComposition } from './api.js'; +import { checkSerenitySuiteConventions } from './serenity.js'; +import { checkUiAppComposition } from './ui-app.js'; + +const fixturesRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'architecture-stubs'); + +describe('architecture test stub rejection', () => { + it('rejects the incomplete API stub', async () => { + const violations = await checkApiComposition({ apiIndexPath: path.join(fixturesRoot, 'api', 'src', 'index.ts') }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('initializeInfrastructureServices'), expect.stringContaining('initializeApplicationServices'), expect.stringContaining('registerAzureFunction')])); + }); + + it('rejects the incomplete UI stub', async () => { + const violations = await checkUiAppComposition({ + appRoot: path.join(fixturesRoot, 'ui', 'src'), + requiredProviders: ['ThemeProvider', 'BrowserRouter', 'AuthProvider'], + }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('React root'), expect.stringContaining('ThemeProvider'), expect.stringContaining('Routes')])); + }); + + it('rejects the incomplete Serenity stub', async () => { + const violations = await checkSerenitySuiteConventions({ + suiteRoot: path.join(fixturesRoot, 'serenity', 'src'), + requireManagedCleanup: true, + }); + + expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('Managed world'), expect.stringContaining('at least one server'), expect.stringContaining('delegate behavior')])); + }); +}); diff --git a/packages/cellix/archunit-tests/src/test-suites/api-composition.ts b/packages/cellix/archunit-tests/src/test-suites/api-composition.ts new file mode 100644 index 000000000..2c4d11016 --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/api-composition.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { type ApiCompositionConfig, checkApiComposition } from '../checks/api-composition.js'; + +/** + * Register reusable fitness tests for a Cellix API composition root. + * @param config - Location of the consumer's API composition entrypoint. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeApiCompositionTests({ apiIndexPath: 'src/index.ts' });` + */ +export function describeApiCompositionTests(config: ApiCompositionConfig): void { + describe('Cellix API composition', () => { + it('initializes infrastructure, context, application services, handlers, and startup in order', async () => { + expect(await checkApiComposition(config)).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts new file mode 100644 index 000000000..fd9c1033c --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/serenity-suite-conventions.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { checkSerenitySuiteConventions, type SerenitySuiteConventionsConfig } from '../checks/serenity-suite-conventions.js'; + +/** + * Register reusable fitness tests for a Cellix Serenity/Cucumber suite. + * @param config - Suite source root and cleanup policy. Context directories are discovered automatically. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeSerenitySuiteConventionTests({ suiteRoot: 'src', requireManagedCleanup: true });` + */ +export function describeSerenitySuiteConventionTests(config: SerenitySuiteConventionsConfig): void { + describe('Cellix Serenity suite conventions', () => { + it('uses a managed world and lifecycle', async () => { + const violations = await checkSerenitySuiteConventions(config); + expect(violations.filter((violation) => violation.includes('world.ts') || violation.includes('lifecycle'))).toStrictEqual([]); + }); + + it('loads context indexes and delegates steps to Screenplay abstractions', async () => { + const violations = await checkSerenitySuiteConventions(config); + expect(violations.filter((violation) => !violation.includes('world.ts') && !violation.includes('lifecycle'))).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts b/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts new file mode 100644 index 000000000..ab689c9bc --- /dev/null +++ b/packages/cellix/archunit-tests/src/test-suites/ui-app-composition.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { checkUiAppComposition, type UiAppCompositionConfig } from '../checks/ui-app-composition.js'; + +/** + * Register reusable fitness tests for a Cellix browser application entrypoint. + * @param config - Source root and providers required by the consumer application. + * @returns Nothing; tests are registered with the active Vitest suite. + * @example `describeUiAppCompositionTests({ appRoot: 'src', requiredProviders: ['BrowserRouter'] });` + */ +export function describeUiAppCompositionTests(config: UiAppCompositionConfig): void { + describe('Cellix UI application composition', () => { + it('validates the mount point and composes providers, the app, and routes', async () => { + expect(await checkUiAppComposition(config)).toStrictEqual([]); + }); + }); +} diff --git a/packages/cellix/archunit-tests/src/ui-app.ts b/packages/cellix/archunit-tests/src/ui-app.ts new file mode 100644 index 000000000..6f8ee3f2d --- /dev/null +++ b/packages/cellix/archunit-tests/src/ui-app.ts @@ -0,0 +1,3 @@ +export type { UiAppCompositionConfig } from './checks/ui-app-composition.js'; +export { checkUiAppComposition } from './checks/ui-app-composition.js'; +export { describeUiAppCompositionTests } from './test-suites/ui-app-composition.js'; diff --git a/packages/cellix/archunit-tests/tsconfig.json b/packages/cellix/archunit-tests/tsconfig.json index 8dbf44f5b..7a0e79572 100644 --- a/packages/cellix/archunit-tests/tsconfig.json +++ b/packages/cellix/archunit-tests/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "." }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "exclude": ["node_modules", "dist", "**/*.test.ts", "src/fixtures/architecture-stubs/**"] } diff --git a/packages/cellix/serenity-framework/src/settings/timeout-settings.ts b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts index 17abeeaac..1e20587b6 100644 --- a/packages/cellix/serenity-framework/src/settings/timeout-settings.ts +++ b/packages/cellix/serenity-framework/src/settings/timeout-settings.ts @@ -5,6 +5,15 @@ export const defaultVerificationTimeouts = { /** Default Cucumber scenario timeout. */ scenario: 120_000, + /** + * Cold-boot timeout for the first `Before` hook, which starts every server + * (Mongo, Azurite, API, UI portals, mock auth) and launches the browser. + * Larger than {@link serverStartup} because a cold run also pays for func + * TypeScript compilation and Vite dependency pre-bundling; only the first + * scenario incurs it (later `Before`s reuse the running infrastructure). + */ + boot: 300_000, + /** Server startup timeout. */ serverStartup: 120_000, diff --git a/packages/cellix/server-oauth2-mock-seedwork/tests/file-user-store.test.ts b/packages/cellix/server-oauth2-mock-seedwork/tests/file-user-store.test.ts index 04772575f..f6fdee1ca 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/tests/file-user-store.test.ts +++ b/packages/cellix/server-oauth2-mock-seedwork/tests/file-user-store.test.ts @@ -126,9 +126,7 @@ describe('file user store oidcConfigName filtering', () => { it('users without oidcConfigName are visible to all scoped stores', async () => { if (!tmp) throw new Error('tmp not created'); - writeUsers(tmp, 'mock-oidc.users.json', [ - { username: 'shared@example.com', sub: 'sub-shared' }, - ]); + writeUsers(tmp, 'mock-oidc.users.json', [{ username: 'shared@example.com', sub: 'sub-shared' }]); const endStore = createFileUserStore(tmp, 'end-user'); const staffStore = createFileUserStore(tmp, 'staff-user'); @@ -191,15 +189,7 @@ describe('file user store oidcConfigName filtering', () => { expect(users).toHaveLength(1); expect(users[0]?.username).toBe('good@example.com'); expect(warnSpy.length).toBeGreaterThan(0); - expect( - warnSpy.some((args) => - (args as unknown[]).some( - (arg) => - typeof arg === 'string' && - arg.includes('"oidcConfigName" must be a string'), - ), - ), - ).toBe(true); + expect(warnSpy.some((args) => (args as unknown[]).some((arg) => typeof arg === 'string' && arg.includes('"oidcConfigName" must be a string')))).toBe(true); } finally { console.warn = origWarn; } diff --git a/packages/cellix/server-oauth2-mock-seedwork/tests/portal-discovery.test.ts b/packages/cellix/server-oauth2-mock-seedwork/tests/portal-discovery.test.ts index d364915c8..565ece8ac 100644 --- a/packages/cellix/server-oauth2-mock-seedwork/tests/portal-discovery.test.ts +++ b/packages/cellix/server-oauth2-mock-seedwork/tests/portal-discovery.test.ts @@ -569,11 +569,7 @@ describe('discoverPortalConfigs', () => { envVars: { clientId: 'VITE_APP_PORTAL_STAFF_CLIENTID', redirectUri: 'VITE_APP_PORTAL_STAFF_REDIRECT' }, }, ]); - writeEnv( - tmp, - 'ui-portal/.env', - 'VITE_APP_PORTAL_END_CLIENTID=end-cid\nVITE_APP_PORTAL_END_REDIRECT=https://portal/end/cb\nVITE_APP_PORTAL_STAFF_CLIENTID=staff-cid\nVITE_APP_PORTAL_STAFF_REDIRECT=https://portal/staff/cb\n', - ); + writeEnv(tmp, 'ui-portal/.env', 'VITE_APP_PORTAL_END_CLIENTID=end-cid\nVITE_APP_PORTAL_END_REDIRECT=https://portal/end/cb\nVITE_APP_PORTAL_STAFF_CLIENTID=staff-cid\nVITE_APP_PORTAL_STAFF_REDIRECT=https://portal/staff/cb\n'); const portals = discoverPortalConfigs(tmp); expect(portals).toHaveLength(2); diff --git a/packages/ocom-verification/acceptance-api/package.json b/packages/ocom-verification/acceptance-api/package.json index 1f4d662af..0ffa8a125 100644 --- a/packages/ocom-verification/acceptance-api/package.json +++ b/packages/ocom-verification/acceptance-api/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "test:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --format json:./reports/cucumber-report-api.json", "test:acceptance:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 --allowExternal -- cucumber-js --format json:./reports/cucumber-report-api.json", "test:coverage:acceptance": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 -- cucumber-js --format json:./reports/cucumber-report-api.json", @@ -25,7 +26,9 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@cellix/service-blob-storage": "workspace:*", "@ocom/application-services": "workspace:*", "@ocom/context-spec": "workspace:*", @@ -43,6 +46,7 @@ "c8": "^11.0.0", "rimraf": "^6.0.1", "tsx": "^4.20.3", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..0459db109 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,6 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts new file mode 100644 index 000000000..4abd0118f --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/notes/staff-notes.ts @@ -0,0 +1,3 @@ +export interface StaffApiNotes { + targetRoute: string; +} diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts new file mode 100644 index 000000000..119a2807c --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/questions/staff-target-route.ts @@ -0,0 +1,4 @@ +import { notes, Question } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; + +export const StaffTargetRoute = () => Question.about('the target staff route', (actor) => actor.answer(notes().get('targetRoute'))); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts index 475769e42..303a82b36 100644 --- a/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/step-definitions/staff-landing.steps.ts @@ -1,13 +1,12 @@ import { Given, Then, When } from '@cucumber/cucumber'; import { actors } from '@ocom-verification/verification-shared/test-data'; import { actorCalled, notes } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; +import { StaffTargetRoute } from '../questions/staff-target-route.ts'; +import { OpenStaffLanding } from '../tasks/open-staff-landing.ts'; type StaffBusinessRole = 'finance' | 'tech admin' | 'service line owner' | 'case manager'; -interface StaffApiNotes { - targetRoute: string; -} - const defaultRouteByRole: Record = { finance: '/staff/finance', 'tech admin': '/staff/tech', @@ -51,19 +50,19 @@ Given('{word} is an authenticated {string} staff user', async (actorName: string When('{word} enters the staff operations workspace', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('targetRoute', defaultRouteByRole[roleForActor(actorName)])); + await actor.attemptsTo(OpenStaffLanding(defaultRouteByRole[roleForActor(actorName)])); }); When('{word} attempts to work in the finance workspace', async (actorName: string) => { lastActorName = actorName; const actor = actorCalled(actorName); - await actor.attemptsTo(notes().set('targetRoute', resolveFinanceWorkspaceRoute(roleForActor(actorName)))); + await actor.attemptsTo(OpenStaffLanding(resolveFinanceWorkspaceRoute(roleForActor(actorName)))); }); Then('{word} should be directed to {string}', async (actorName: string, expectedRoute: string) => { const resolvedName = /^(she|he|they)$/i.test(actorName) ? lastActorName : actorName; const actor = actorCalled(resolvedName); - const targetRoute = await actor.answer(notes().get('targetRoute')); + const targetRoute = await actor.answer(StaffTargetRoute()); if (targetRoute !== expectedRoute) { throw new Error(`Expected route to be "${expectedRoute}", but got "${targetRoute}"`); diff --git a/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts b/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts new file mode 100644 index 000000000..6a482a8f6 --- /dev/null +++ b/packages/ocom-verification/acceptance-api/src/contexts/staff/tasks/open-staff-landing.ts @@ -0,0 +1,7 @@ +import { type Actor, Interaction, notes } from '@serenity-js/core'; +import type { StaffApiNotes } from '../notes/staff-notes.ts'; + +export const OpenStaffLanding = (targetRoute: string) => + Interaction.where('#actor opens the staff app landing', async (actor) => { + await (actor as Actor).attemptsTo(notes().set('targetRoute', targetRoute)); + }); diff --git a/packages/ocom-verification/acceptance-api/vitest.arch.config.ts b/packages/ocom-verification/acceptance-api/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/acceptance-api/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom-verification/acceptance-ui/package.json b/packages/ocom-verification/acceptance-ui/package.json index d0943ce3d..343d54d5d 100644 --- a/packages/ocom-verification/acceptance-ui/package.json +++ b/packages/ocom-verification/acceptance-ui/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "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" @@ -26,7 +27,9 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@ocom-verification/verification-shared": "workspace:*", "@testing-library/react": "^16.3.0", "@types/node": "catalog:", @@ -34,6 +37,7 @@ "@types/react-dom": "^19.1.6", "c8": "^10.1.3", "tsx": "^4.20.3", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..e98032542 --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,5 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', +}); diff --git a/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts b/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/acceptance-ui/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom-verification/e2e-tests/package.json b/packages/ocom-verification/e2e-tests/package.json index 620767bff..23b5c1fe4 100644 --- a/packages/ocom-verification/e2e-tests/package.json +++ b/packages/ocom-verification/e2e-tests/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "test:arch": "vitest run --config vitest.arch.config.ts", "test:e2e": "pnpm run portless:prune && pnpm run proxy:start && pnpm run test:e2e:run", "test:e2e:ci": "pnpm run portless:prune && pnpm run proxy:start:ci && 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", @@ -25,13 +26,16 @@ "std-env": "^4.0.0" }, "devDependencies": { + "@cellix/archunit-tests": "workspace:*", "@ocom/service-queue-storage": "workspace:*", "@cellix/config-typescript": "workspace:*", + "@cellix/config-vitest": "workspace:*", "@ocom-verification/verification-shared": "workspace:*", "@types/node": "catalog:", "playwright": "catalog:", "rimraf": "^6.0.1", "tsx": "^4.20.3", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts b/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts new file mode 100644 index 000000000..0459db109 --- /dev/null +++ b/packages/ocom-verification/e2e-tests/src/archunit-tests/serenity-conventions.test.ts @@ -0,0 +1,6 @@ +import { describeSerenitySuiteConventionTests } from '@cellix/archunit-tests/serenity'; + +describeSerenitySuiteConventionTests({ + suiteRoot: 'src', + requireManagedCleanup: true, +}); 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 3b5b23a65..ddff97239 100644 --- a/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts +++ b/packages/ocom-verification/e2e-tests/src/cucumber-lifecycle-hooks.ts @@ -13,6 +13,9 @@ const currentDir = fileURLToPath(new URL('.', import.meta.url)); export function registerLifecycleHooks(): void { registerWorldLifecycleHooks({ scenarioTimeout: getTimeout('scenario'), + // The first Before cold-boots all servers + the browser, which exceeds the + // per-scenario budget on a cold machine; give it a dedicated boot timeout. + beforeTimeout: getTimeout('boot'), before: async (world) => { await world.init(); }, 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 56c41feb2..41a9dcf2c 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 @@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { getAzuritePorts, getMongoPort } from '@ocom-verification/verification-shared/environment'; import { getPortlessPath } from './resolve-portless.ts'; let proxyInitialized = false; @@ -76,6 +77,51 @@ function loadE2EEnvDefaults(): void { process.env[key] ??= trimmed.slice(idx + 1); } } + + applyWorktreeServiceEnv(); +} + +/** + * Retarget the pinned service settings at the current worktree's ports and host. + * + * `local-settings.e2e.json` pins the default Azurite ports (10000–10002), the + * default Mongo port (50000), and the un-suffixed mock-auth host. In worktree + * mode the test servers instead bind the offset ports from + * {@link getAzuritePorts}/{@link getMongoPort} and register the + * `.${WORKTREE_NAME}.localhost` host. Because `test-environment.ts` seeds these + * values into `process.env`, the pinned defaults leak into the spawned Functions + * host (defeating the `??=` worktree overrides in `apps/api/start-dev.mjs`) and + * into the queue verification client — so the API blocks on Mongo/queue startup + * against the wrong ports and token validation hits the wrong host. + * + * No-op when `WORKTREE_NAME` is unset (offset 0, host unchanged). + */ +function applyWorktreeServiceEnv(): void { + const worktreeName = process.env['WORKTREE_NAME']; + if (!worktreeName) return; + + const { blob, queue, table } = getAzuritePorts(); + const azurite = process.env['AZURE_STORAGE_CONNECTION_STRING']; + if (azurite) { + const worktreeAzurite = azurite.replace('127.0.0.1:10000', `127.0.0.1:${blob}`).replace('127.0.0.1:10001', `127.0.0.1:${queue}`).replace('127.0.0.1:10002', `127.0.0.1:${table}`); + process.env['AZURE_STORAGE_CONNECTION_STRING'] = worktreeAzurite; + // `AzureWebJobsStorage` is pinned to `UseDevelopmentStorage=true` (default + // ports); replace it with the explicit worktree connection string so the + // Functions host targets the offset ports, matching start-dev.mjs. + process.env['AzureWebJobsStorage'] = worktreeAzurite; + } + + const mongo = process.env['COSMOSDB_CONNECTION_STRING']; + if (mongo) { + process.env['COSMOSDB_CONNECTION_STRING'] = mongo.replace('127.0.0.1:50000', `127.0.0.1:${getMongoPort()}`); + } + + // The mock-auth OIDC endpoints/issuers are pinned to the un-suffixed host; + // retarget them at the worktree host so JWKS fetches reach the running server. + for (const key of ['ACCOUNT_PORTAL_OIDC_ENDPOINT', 'ACCOUNT_PORTAL_OIDC_ISSUER', 'STAFF_PORTAL_OIDC_ENDPOINT', 'STAFF_PORTAL_OIDC_ISSUER']) { + const value = process.env[key]; + if (value) process.env[key] = value.replace('.localhost', `.${worktreeName}.localhost`); + } } function loadApiLocalSettings(filePath: string): void { diff --git a/packages/ocom-verification/e2e-tests/vitest.arch.config.ts b/packages/ocom-verification/e2e-tests/vitest.arch.config.ts new file mode 100644 index 000000000..c9624eb8f --- /dev/null +++ b/packages/ocom-verification/e2e-tests/vitest.arch.config.ts @@ -0,0 +1,3 @@ +import { archConfig } from '@cellix/config-vitest'; + +export default archConfig; diff --git a/packages/ocom/service-queue-storage/turbo.json b/packages/ocom/service-queue-storage/turbo.json index 5cf61f325..44aa7bf51 100644 --- a/packages/ocom/service-queue-storage/turbo.json +++ b/packages/ocom/service-queue-storage/turbo.json @@ -8,7 +8,7 @@ "outputs": ["src/**/*.schema.generated.ts"] }, "build": { - "dependsOn": ["gen"], + "dependsOn": ["^build", "gen"], "inputs": ["$TURBO_EXTENDS$"], "outputs": ["$TURBO_EXTENDS$"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf48ef53c..ebf9dac91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,9 +129,8 @@ 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@1: 2.1.1 + brace-expansion@5: 5.0.7 diff@4.0.2: 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -175,7 +174,7 @@ overrides: form-data: ^4.0.6 webpack-dev-server>http-proxy-middleware: 3.0.7 joi: ^17.13.4 - brace-expansion@2: 2.0.3 + brace-expansion@2: 2.1.1 dottie: 2.0.7 http-proxy-middleware: 3.0.7 immutable: 3.8.3 @@ -332,6 +331,9 @@ importers: specifier: 1.9.0 version: 1.9.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-rolldown': specifier: workspace:* version: link:../../packages/cellix/config-rolldown @@ -543,6 +545,9 @@ importers: specifier: 'catalog:' version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../packages/cellix/config-typescript @@ -670,6 +675,9 @@ importers: specifier: 'catalog:' version: 7.15.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../packages/cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../packages/cellix/config-typescript @@ -1268,9 +1276,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@cellix/service-blob-storage': specifier: workspace:* version: link:../../cellix/service-blob-storage @@ -1325,6 +1339,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + 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.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-ui: dependencies: @@ -1371,9 +1388,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@ocom-verification/verification-shared': specifier: workspace:* version: link:../verification-shared @@ -1398,6 +1421,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + 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.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/archunit-tests: devDependencies: @@ -1447,9 +1473,15 @@ importers: specifier: ^4.0.0 version: 4.0.0 devDependencies: + '@cellix/archunit-tests': + specifier: workspace:* + version: link:../../cellix/archunit-tests '@cellix/config-typescript': specifier: workspace:* version: link:../../cellix/config-typescript + '@cellix/config-vitest': + specifier: workspace:* + version: link:../../cellix/config-vitest '@ocom-verification/verification-shared': specifier: workspace:* version: link:../verification-shared @@ -1471,6 +1503,9 @@ importers: typescript: specifier: 'catalog:' version: 6.0.3 + vitest: + specifier: 'catalog:' + 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.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/verification-shared: dependencies: @@ -7818,14 +7853,11 @@ packages: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} - brace-expansion@1.1.13: - resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - 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: @@ -8258,9 +8290,6 @@ packages: compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concurrently@9.2.1: resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} engines: {node: '>=18'} @@ -20606,16 +20635,11 @@ snapshots: widest-line: 4.0.1 wrap-ansi: 8.1.0 - brace-expansion@1.1.13: + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.0.3: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -21072,8 +21096,6 @@ snapshots: compute-scroll-into-view@3.1.1: {} - concat-map@0.0.1: {} - concurrently@9.2.1: dependencies: chalk: 4.1.2 @@ -24189,15 +24211,15 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.13 + brace-expansion: 2.1.1 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.3 + brace-expansion: 2.1.1 minimist@1.2.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a5e8c4715..c4085ba35 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -80,9 +80,8 @@ 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@1': 2.1.1 + 'brace-expansion@5': 5.0.7 'diff@4.0.2': 4.0.4 '@protobufjs/codegen': 2.0.5 '@protobufjs/utf8': 1.1.1 @@ -126,7 +125,7 @@ overrides: form-data: ^4.0.6 'webpack-dev-server>http-proxy-middleware': 3.0.7 joi: ^17.13.4 - brace-expansion@2: 2.0.3 + brace-expansion@2: 2.1.1 dottie: 2.0.7 http-proxy-middleware: 3.0.7 immutable: 3.8.3 From 5a0bed8bbabad8fb79dd5750336e667ac6d5828c Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 1 Jul 2026 15:47:25 -0400 Subject: [PATCH 03/10] sourcery feedback applied --- .../application-composition.contract.test.ts | 47 ++++++++ .../src/checks/api-composition.ts | 19 +-- .../src/checks/serenity-suite-conventions.ts | 98 +++++++++------- .../src/checks/typescript-source.ts | 108 ++++++++++++++++++ .../src/checks/ui-app-composition.ts | 22 ++-- .../src/serenity.contract.test.ts | 29 +++++ 6 files changed, 266 insertions(+), 57 deletions(-) create mode 100644 packages/cellix/archunit-tests/src/checks/typescript-source.ts diff --git a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts index d7cf0878a..b95b09cec 100644 --- a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts +++ b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts @@ -50,6 +50,33 @@ Cellix.initializeInfrastructureServices((registry) => registry.registerInfrastru expect(await checkApiComposition({ apiIndexPath })).toStrictEqual([]); }); + it('accepts aliased API composition imports and multiline callbacks', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix as Application } from './cellix.ts'; +Application.initializeInfrastructureServices( + registry => registry.registerInfrastructureService(service), +) + .setContext(registry => ({ registry })) + .initializeApplicationServices(context => build(context)) + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .startUp();`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { + private setupLifecycle() { + app.hook.appStart(async () => { + await this.startAllServicesWithTracing(); + this.contextInternal = this.contextCreatorInternal(this); + }); + } +}`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toStrictEqual([]); + }); + it('reports omitted and out-of-order API startup stages', async () => { const apiIndexPath = await fixture('index.ts', `import { Cellix } from './cellix.ts';\nCellix.startUp().setContext(() => ({}));`); @@ -89,6 +116,26 @@ createRoot(root).render(< expect(await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider', 'BrowserRouter'] })).toStrictEqual([]); }); + it('accepts aliased React Router components', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (mount === null) throw new Error('missing root'); +createRoot(mount).render();`, + ), + ); + await writeFile( + path.join(appRoot, 'App.tsx'), + `import { Routes as RouteSwitch, Route as RouteEntry } from 'react-router-dom'; +export default () => } />;`, + ); + + expect(await checkUiAppComposition({ appRoot })).toStrictEqual([]); + }); + it('reports a UI entrypoint that skips root validation, providers, or routing', async () => { const appRoot = path.dirname(await fixture('src/main.tsx', `import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render();`)); await writeFile(path.join(appRoot, 'App.tsx'), 'export default () =>
;'); diff --git a/packages/cellix/archunit-tests/src/checks/api-composition.ts b/packages/cellix/archunit-tests/src/checks/api-composition.ts index 22f2b24c2..8c1c84356 100644 --- a/packages/cellix/archunit-tests/src/checks/api-composition.ts +++ b/packages/cellix/archunit-tests/src/checks/api-composition.ts @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; +import { callbackContainsCall, callbackReturnsObject, callPositions, findImportedBinding, parseTypeScript } from './typescript-source.js'; export interface ApiCompositionConfig { /** Path to the API composition root, normally `src/index.ts`. */ @@ -31,11 +32,14 @@ export async function checkApiComposition(config: ApiCompositionConfig): Promise } const violations: string[] = []; - if (!/import\s+\{[^}]*\bCellix\b[^}]*\}\s+from\s+['"][^'"]+['"]/.test(content)) { + const source = parseTypeScript(config.apiIndexPath, content); + const cellixBinding = findImportedBinding(source, 'Cellix'); + if (!cellixBinding) { violations.push(`[${config.apiIndexPath}] API composition root must import Cellix`); } - const positions = requiredStages.map((stage) => ({ stage, position: content.indexOf(`.${stage}`) })); + const syntaxPositions = callPositions(source, requiredStages); + const positions = requiredStages.map((stage) => ({ stage, position: syntaxPositions.get(stage) ?? -1 })); for (const { stage, position } of positions) { if (position < 0) { violations.push(`[${config.apiIndexPath}] API composition root must call ${stage}${stage === 'registerAzureFunction' ? '* for at least one handler' : ''}`); @@ -51,24 +55,23 @@ export async function checkApiComposition(config: ApiCompositionConfig): Promise } } - if (!/initializeInfrastructureServices(?:<[^;]+?>)?\s*\(\s*\(?\s*\w+\s*\)?\s*=>[\s\S]*?registerInfrastructureService/.test(content)) { + if (!callbackContainsCall(source, 'initializeInfrastructureServices', 'registerInfrastructureService')) { violations.push(`[${config.apiIndexPath}] initializeInfrastructureServices must register at least one infrastructure service`); } - if (!/\.setContext\s*\(\s*\(?\s*\w+\s*\)?\s*=>[\s\S]*?return\s*\{/.test(content)) { + if (!callbackReturnsObject(source, 'setContext')) { violations.push(`[${config.apiIndexPath}] setContext must build and return the runtime context`); } - await checkCellixInfrastructureLifecycle(config.apiIndexPath, content, violations); + await checkCellixInfrastructureLifecycle(config.apiIndexPath, cellixBinding?.moduleName, violations); return violations; } -async function checkCellixInfrastructureLifecycle(apiIndexPath: string, apiIndex: string, violations: string[]): Promise { - const cellixImport = apiIndex.match(/import\s+\{[^}]*\bCellix\b[^}]*\}\s+from\s+['"](\.\/[^'"]+)['"]/); - const importPath = cellixImport?.[1]; +async function checkCellixInfrastructureLifecycle(apiIndexPath: string, importPath: string | undefined, violations: string[]): Promise { if (!importPath) { return; } + if (!importPath.startsWith('.')) return; const cellixPath = path.resolve(path.dirname(apiIndexPath), importPath); let cellix: string; diff --git a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts index 25a9080b1..2f6e8dab8 100644 --- a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts +++ b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts @@ -1,5 +1,6 @@ import { access, readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; +import { containsCall, containsNewExpression, localImportName, parseTypeScript } from './typescript-source.js'; export interface SerenitySuiteConventionsConfig { /** Source root containing the suite world, lifecycle hooks, and contexts. */ @@ -45,13 +46,16 @@ export async function checkSerenitySuiteConventions(config: SerenitySuiteConvent if (await exists(worldPath)) { const world = await readFile(worldPath, 'utf8'); - if (!/import\s+\{[^}]*registerManagedSerenityWorld[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/cucumber['"]/.test(world)) { + const source = parseTypeScript(worldPath, world); + const registerWorld = localImportName(source, '@cellix/serenity-framework/cucumber', 'registerManagedSerenityWorld'); + const serenityCast = localImportName(source, '@cellix/serenity-framework/serenity', 'SerenityCast'); + if (!registerWorld || !containsCall(source, registerWorld)) { violations.push(`[${worldPath}] Managed world must use registerManagedSerenityWorld from @cellix/serenity-framework/cucumber`); } - if (!/import\s+\{[^}]*SerenityCast[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/serenity['"]/.test(world)) { + if (!serenityCast || !containsNewExpression(source, serenityCast)) { violations.push(`[${worldPath}] Managed world must use SerenityCast from @cellix/serenity-framework/serenity`); } - for (const required of ['registerManagedSerenityWorld', 'new SerenityCast', 'createCast:', 'useNotepad: true', 'registerLifecycleHooks()']) { + for (const required of ['createCast:', 'useNotepad: true', 'registerLifecycleHooks()']) { if (!world.includes(required)) { violations.push(`[${worldPath}] Managed world must include ${required}`); } @@ -63,21 +67,22 @@ export async function checkSerenitySuiteConventions(config: SerenitySuiteConvent if (await exists(lifecyclePath)) { const lifecycle = await readFile(lifecyclePath, 'utf8'); - if (!/import\s+\{[^}]*registerWorldLifecycleHooks[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/cucumber['"]/.test(lifecycle)) { + const source = parseTypeScript(lifecyclePath, lifecycle); + const registerHooks = localImportName(source, '@cellix/serenity-framework/cucumber', 'registerWorldLifecycleHooks'); + const getTimeout = localImportName(source, '@cellix/serenity-framework/settings', 'getTimeout'); + if (!registerHooks || !containsCall(source, registerHooks)) { violations.push(`[${lifecyclePath}] Lifecycle must use registerWorldLifecycleHooks from @cellix/serenity-framework/cucumber`); } - if (!/import\s+\{[^}]*getTimeout[^}]*\}\s+from\s+['"]@cellix\/serenity-framework\/settings['"]/.test(lifecycle)) { + if (!getTimeout || !containsCall(source, getTimeout)) { violations.push(`[${lifecyclePath}] Lifecycle must use shared timeout settings`); } - for (const required of ['registerWorldLifecycleHooks', "getTimeout('scenario')", 'world.init()']) { - if (!lifecycle.includes(required)) { - violations.push(`[${lifecyclePath}] Lifecycle hooks must include ${required}`); - } + if (!containsCall(source, 'init')) { + violations.push(`[${lifecyclePath}] Lifecycle hooks must include world.init()`); } - if (config.requireManagedCleanup && !lifecycle.includes('world.cleanup()')) { + if (config.requireManagedCleanup && !containsCall(source, 'cleanup')) { violations.push(`[${lifecyclePath}] Managed infrastructure suites must clean up the world after each scenario`); } - if (config.requireManagedCleanup && !/afterAll:\s*\(\)\s*=>\s*infrastructure\.stopAll\(\)/.test(lifecycle)) { + if (config.requireManagedCleanup && !containsCall(source, 'stopAll')) { violations.push(`[${lifecyclePath}] Managed infrastructure suites must stop all infrastructure after the suite`); } } @@ -90,9 +95,8 @@ export async function checkSerenitySuiteConventions(config: SerenitySuiteConvent if (contexts.length === 0) { violations.push(`[${contextsRoot}] Serenity suite must define at least one context directory`); } - for (const context of contexts) { - await checkContext(config.suiteRoot, context, loaderPath, loader, violations); - } + const contextViolations = await Promise.all(contexts.map((context) => checkContext(config.suiteRoot, context, loaderPath, loader))); + violations.push(...contextViolations.flat()); return violations; } @@ -137,7 +141,8 @@ async function discoverContexts(contextsRoot: string): Promise { } } -async function checkContext(suiteRoot: string, context: string, loaderPath: string, loader: string, violations: string[]): Promise { +async function checkContext(suiteRoot: string, context: string, loaderPath: string, loader: string): Promise { + const violations: string[] = []; const contextRoot = path.join(suiteRoot, 'contexts', context); const contextIndex = path.join(contextRoot, 'step-definitions', 'index.ts'); if (!loader.includes(`../contexts/${context}/step-definitions/index.ts`)) { @@ -145,7 +150,7 @@ async function checkContext(suiteRoot: string, context: string, loaderPath: stri } if (!(await exists(contextIndex))) { violations.push(`[${contextIndex}] Context step-definition index must exist`); - return; + return violations; } const stepDirectory = path.dirname(contextIndex); @@ -155,31 +160,42 @@ async function checkContext(suiteRoot: string, context: string, loaderPath: stri } const indexContent = await readFile(contextIndex, 'utf8'); - for (const stepFile of stepFiles) { - if (!indexContent.includes(`./${stepFile}`)) { - violations.push(`[${contextIndex}] Context index must import ${stepFile}`); - } - const stepPath = path.join(stepDirectory, stepFile); - const stepContent = await readFile(stepPath, 'utf8'); - if (!/from\s+['"]@cucumber\/cucumber['"]/.test(stepContent)) { - violations.push(`[${stepPath}] Step definitions must use Cucumber`); - } - const abstractionImports = [...stepContent.matchAll(/from\s+['"]\.\.\/(tasks|questions)\/([^'"]+)['"]/g)]; - if (abstractionImports.length === 0 || !/\.(attemptsTo|answer)\s*\(/.test(stepContent)) { - violations.push(`[${stepPath}] Step definitions must delegate behavior to a task, interaction, or question`); - } - for (const match of abstractionImports) { - const relativeFile = match[2]; - if (!relativeFile) continue; - const abstractionPath = path.join(contextRoot, match[1] as string, relativeFile); - if (!(await exists(abstractionPath))) { - violations.push(`[${abstractionPath}] Imported Screenplay abstraction must exist`); - continue; + const stepViolations = await Promise.all( + stepFiles.sort().map(async (stepFile) => { + const discovered: string[] = []; + if (!indexContent.includes(`./${stepFile}`)) { + discovered.push(`[${contextIndex}] Context index must import ${stepFile}`); } - const abstraction = await readFile(abstractionPath, 'utf8'); - if (!/from\s+['"]@serenity-js\/core['"]/.test(abstraction) || !/\b(Task|Interaction|Question)\b/.test(abstraction)) { - violations.push(`[${abstractionPath}] Context tasks and questions must implement a Serenity Screenplay abstraction`); + const stepPath = path.join(stepDirectory, stepFile); + const stepContent = await readFile(stepPath, 'utf8'); + if (!/from\s+['"]@cucumber\/cucumber['"]/.test(stepContent)) { + discovered.push(`[${stepPath}] Step definitions must use Cucumber`); } - } - } + const abstractionImports = [...stepContent.matchAll(/from\s+['"]\.\.\/(tasks|questions)\/([^'"]+)['"]/g)]; + if (abstractionImports.length === 0 || !/\.(attemptsTo|answer)\s*\(/.test(stepContent)) { + discovered.push(`[${stepPath}] Step definitions must delegate behavior to a task, interaction, or question`); + } + const abstractionViolations = await Promise.all( + abstractionImports.map(async (match) => { + const abstractionViolations: string[] = []; + const relativeFile = match[2]; + if (!relativeFile) return abstractionViolations; + const abstractionPath = path.join(contextRoot, match[1] as string, relativeFile); + if (!(await exists(abstractionPath))) { + abstractionViolations.push(`[${abstractionPath}] Imported Screenplay abstraction must exist`); + return abstractionViolations; + } + const abstraction = await readFile(abstractionPath, 'utf8'); + if (!/from\s+['"]@serenity-js\/core['"]/.test(abstraction) || !/\b(Task|Interaction|Question)\b/.test(abstraction)) { + abstractionViolations.push(`[${abstractionPath}] Context tasks and questions must implement a Serenity Screenplay abstraction`); + } + return abstractionViolations; + }), + ); + discovered.push(...abstractionViolations.flat()); + return discovered; + }), + ); + violations.push(...stepViolations.flat()); + return violations; } diff --git a/packages/cellix/archunit-tests/src/checks/typescript-source.ts b/packages/cellix/archunit-tests/src/checks/typescript-source.ts new file mode 100644 index 000000000..2eb489119 --- /dev/null +++ b/packages/cellix/archunit-tests/src/checks/typescript-source.ts @@ -0,0 +1,108 @@ +import ts from 'typescript'; + +interface ImportedBinding { + imported: string; + local: string; +} + +export function parseTypeScript(filePath: string, content: string): ts.SourceFile { + return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, filePath.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS); +} + +function importedBindings(source: ts.SourceFile, moduleName: string): ImportedBinding[] { + const bindings: ImportedBinding[] = []; + for (const statement of source.statements) { + if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== moduleName) continue; + for (const element of statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) ? statement.importClause.namedBindings.elements : []) { + bindings.push({ imported: element.propertyName?.text ?? element.name.text, local: element.name.text }); + } + } + return bindings; +} + +export function localImportName(source: ts.SourceFile, moduleName: string, importedName: string): string | undefined { + return importedBindings(source, moduleName).find((binding) => binding.imported === importedName)?.local; +} + +export function findImportedBinding(source: ts.SourceFile, importedName: string): (ImportedBinding & { moduleName: string }) | undefined { + for (const statement of source.statements) { + if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue; + const binding = importedBindings(source, statement.moduleSpecifier.text).find((candidate) => candidate.imported === importedName); + if (binding) return { ...binding, moduleName: statement.moduleSpecifier.text }; + } + return undefined; +} + +export function containsNewExpression(source: ts.Node, name: string): boolean { + return someNode(source, (node) => ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name); +} + +export function containsCall(source: ts.Node, name: string): boolean { + return someNode(source, (node) => { + if (!ts.isCallExpression(node)) return false; + return expressionName(node.expression) === name; + }); +} + +export function callPositions(source: ts.SourceFile, names: readonly string[]): Map { + const positions = new Map(); + someNode(source, (node) => { + if (!ts.isCallExpression(node)) return false; + const name = expressionName(node.expression); + const matchedName = name && names.find((candidate) => name === candidate || (candidate === 'registerAzureFunction' && name.startsWith(candidate))); + if (matchedName && !positions.has(matchedName)) positions.set(matchedName, node.getStart(source)); + return false; + }); + return positions; +} + +export function callbackContainsCall(source: ts.SourceFile, callName: string, nestedCallName: string): boolean { + let matches = false; + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== callName) return false; + matches = node.arguments.some((argument) => (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) && containsCall(argument.body, nestedCallName)); + return matches; + }); + return matches; +} + +export function callbackReturnsObject(source: ts.SourceFile, callName: string): boolean { + let matches = false; + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== callName) return false; + matches = node.arguments.some((argument) => { + if (!ts.isArrowFunction(argument) && !ts.isFunctionExpression(argument)) return false; + if (ts.isParenthesizedExpression(argument.body)) return ts.isObjectLiteralExpression(argument.body.expression); + if (ts.isObjectLiteralExpression(argument.body)) return true; + return ts.isBlock(argument.body) && argument.body.statements.some((statement) => ts.isReturnStatement(statement) && !!statement.expression && ts.isObjectLiteralExpression(statement.expression)); + }); + return matches; + }); + return matches; +} + +export function containsJsxTag(source: ts.SourceFile, tagName: string): boolean { + return someNode(source, (node) => { + if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) return node.tagName.getText(source) === tagName; + return false; + }); +} + +export function containsIfThrow(source: ts.SourceFile): boolean { + return someNode(source, (node) => ts.isIfStatement(node) && someNode(node.thenStatement, ts.isThrowStatement)); +} + +function expressionName(expression: ts.LeftHandSideExpression): string | undefined { + if (ts.isIdentifier(expression)) return expression.text; + if (ts.isPropertyAccessExpression(expression)) return expression.name.text; + return undefined; +} + +function someNode(node: ts.Node, predicate: (node: ts.Node) => boolean): boolean { + if (predicate(node)) return true; + let found = false; + node.forEachChild((child) => { + if (!found && someNode(child, predicate)) found = true; + }); + return found; +} diff --git a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts index 50d5e6110..f54837671 100644 --- a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts +++ b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; +import { containsCall, containsIfThrow, containsJsxTag, localImportName, parseTypeScript } from './typescript-source.js'; export interface UiAppCompositionConfig { /** Directory containing `main.tsx` and `App.tsx`. */ @@ -39,31 +40,36 @@ export async function checkUiAppComposition(config: UiAppCompositionConfig): Pro const app = await readSource(appPath, violations); if (main) { - if (!/createRoot\s*\(/.test(main)) { + const source = parseTypeScript(mainPath, main); + if (!containsCall(source, 'createRoot')) { violations.push(`[${mainPath}] UI bootstrap must create a React root`); } if (!/getElementById\s*\(\s*['"]root['"]\s*\)/.test(main)) { violations.push(`[${mainPath}] UI bootstrap must resolve the #root mount element`); } - if (!/if\s*\(\s*!\s*\w+\s*\)[\s\S]*?throw\s+new\s+Error/.test(main)) { + if (!containsIfThrow(source)) { violations.push(`[${mainPath}] UI bootstrap must guard against a missing root element`); } - if (!//.test(main)) { + if (!containsJsxTag(source, 'App')) { violations.push(`[${mainPath}] UI bootstrap must render App`); } for (const provider of config.requiredProviders ?? []) { - const escapedProvider = provider.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - if (!new RegExp(`<${escapedProvider}\\b`).test(main)) { + if (!containsJsxTag(source, provider)) { violations.push(`[${mainPath}] UI bootstrap must compose ${provider}`); } } } - if (app && (!/from\s+['"]react-router-dom['"]/.test(app) || !/ { expect(await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true })).toStrictEqual([]); }); + it('accepts aliased framework imports and formatted lifecycle callbacks', async () => { + const suiteRoot = await suiteFixture(); + await write( + suiteRoot, + 'world.ts', + `import { registerManagedSerenityWorld as registerWorld } from '@cellix/serenity-framework/cucumber'; +import { SerenityCast as TestCast } from '@cellix/serenity-framework/serenity'; +export const TestWorld = registerWorld({ createCast: () => new TestCast({ useNotepad: true }) }); +export type TestWorld = InstanceType; +registerLifecycleHooks();`, + ); + await write( + suiteRoot, + 'cucumber-lifecycle-hooks.ts', + `import { registerWorldLifecycleHooks as registerHooks } from '@cellix/serenity-framework/cucumber'; +import { getTimeout as timeoutFor } from '@cellix/serenity-framework/settings'; +registerHooks({ + scenarioTimeout: timeoutFor('scenario'), + before: async world => world.init(), + after: async world => world.cleanup(), + afterAll: async () => { + await infrastructure.stopAll(); + }, +});`, + ); + + expect(await checkSerenitySuiteConventions({ suiteRoot, requireManagedCleanup: true })).toStrictEqual([]); + }); + it('reports missing context indexes and steps that contain behavior directly', async () => { const suiteRoot = await suiteFixture(); await write(suiteRoot, 'step-definitions/index.ts', ''); From e8023535659417c87ff144dc8816142aaf029a97 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 1 Jul 2026 16:10:03 -0400 Subject: [PATCH 04/10] restart pipeline --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 6800a68c5..f849acfa1 100644 --- a/readme.md +++ b/readme.md @@ -147,6 +147,7 @@ pnpm run build + Mock OIDC server The local mock OpenID Connect provider used for UI development is configured per-portal via JSON files placed in the UI app directories. From dfd2b82109be3aa80071e7ee791425b679b7c5b4 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Thu, 2 Jul 2026 10:42:51 -0400 Subject: [PATCH 05/10] small adjustments based on the sourcery feedback --- .../application-composition.contract.test.ts | 32 +++++++++++++++++++ .../src/checks/serenity-suite-conventions.ts | 2 +- .../src/checks/typescript-source.ts | 28 ++++++++++++++-- .../src/checks/ui-app-composition.ts | 4 +-- .../src/serenity.contract.test.ts | 5 ++- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts index b95b09cec..fcc5b24cd 100644 --- a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts +++ b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts @@ -136,6 +136,38 @@ export default () => } /> { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +invariant(mount, 'missing root'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toStrictEqual([]); + }); + + it('does not mistake an unrelated if and throw for mount-point validation', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (configuration.invalid) throw new Error('invalid configuration'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must guard against a missing root element`); + }); + it('reports a UI entrypoint that skips root validation, providers, or routing', async () => { const appRoot = path.dirname(await fixture('src/main.tsx', `import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render();`)); await writeFile(path.join(appRoot, 'App.tsx'), 'export default () =>
;'); diff --git a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts index 2f6e8dab8..7edeedb95 100644 --- a/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts +++ b/packages/cellix/archunit-tests/src/checks/serenity-suite-conventions.ts @@ -55,7 +55,7 @@ export async function checkSerenitySuiteConventions(config: SerenitySuiteConvent if (!serenityCast || !containsNewExpression(source, serenityCast)) { violations.push(`[${worldPath}] Managed world must use SerenityCast from @cellix/serenity-framework/serenity`); } - for (const required of ['createCast:', 'useNotepad: true', 'registerLifecycleHooks()']) { + for (const required of ['createCast:', 'useNotepad: true']) { if (!world.includes(required)) { violations.push(`[${worldPath}] Managed world must include ${required}`); } diff --git a/packages/cellix/archunit-tests/src/checks/typescript-source.ts b/packages/cellix/archunit-tests/src/checks/typescript-source.ts index 2eb489119..5e9753f17 100644 --- a/packages/cellix/archunit-tests/src/checks/typescript-source.ts +++ b/packages/cellix/archunit-tests/src/checks/typescript-source.ts @@ -88,8 +88,32 @@ export function containsJsxTag(source: ts.SourceFile, tagName: string): boolean }); } -export function containsIfThrow(source: ts.SourceFile): boolean { - return someNode(source, (node) => ts.isIfStatement(node) && someNode(node.thenStatement, ts.isThrowStatement)); +/** Check whether a resolved DOM mount point is protected by an explicit guard. */ +export function containsMountPointGuard(source: ts.SourceFile, elementId: string): boolean { + const mountNames = new Set(); + someNode(source, (node) => { + if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer) return false; + if (isElementLookup(node.initializer, elementId) || (ts.isCallExpression(node.initializer) && node.initializer.arguments.some((argument) => isElementLookup(argument, elementId)))) { + mountNames.add(node.name.text); + } + return false; + }); + + return someNode(source, (node) => { + if (ts.isIfStatement(node) && referencesAnyIdentifier(node.expression, mountNames)) { + return someNode(node.thenStatement, (child) => ts.isThrowStatement(child) || ts.isCallExpression(child)); + } + if (!ts.isCallExpression(node) || !/^(assert|ensure|guard|invariant|require|validate)/i.test(expressionName(node.expression) ?? '')) return false; + return node.arguments.some((argument) => referencesAnyIdentifier(argument, mountNames) || isElementLookup(argument, elementId)); + }); +} + +function isElementLookup(node: ts.Node, elementId: string): boolean { + return ts.isCallExpression(node) && expressionName(node.expression) === 'getElementById' && node.arguments.some((argument) => ts.isStringLiteral(argument) && argument.text === elementId); +} + +function referencesAnyIdentifier(node: ts.Node, names: ReadonlySet): boolean { + return someNode(node, (child) => ts.isIdentifier(child) && names.has(child.text)); } function expressionName(expression: ts.LeftHandSideExpression): string | undefined { diff --git a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts index f54837671..0c399e38e 100644 --- a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts +++ b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { containsCall, containsIfThrow, containsJsxTag, localImportName, parseTypeScript } from './typescript-source.js'; +import { containsCall, containsJsxTag, containsMountPointGuard, localImportName, parseTypeScript } from './typescript-source.js'; export interface UiAppCompositionConfig { /** Directory containing `main.tsx` and `App.tsx`. */ @@ -47,7 +47,7 @@ export async function checkUiAppComposition(config: UiAppCompositionConfig): Pro if (!/getElementById\s*\(\s*['"]root['"]\s*\)/.test(main)) { violations.push(`[${mainPath}] UI bootstrap must resolve the #root mount element`); } - if (!containsIfThrow(source)) { + if (!containsMountPointGuard(source, 'root')) { violations.push(`[${mainPath}] UI bootstrap must guard against a missing root element`); } if (!containsJsxTag(source, 'React.StrictMode')) { diff --git a/packages/cellix/archunit-tests/src/serenity.contract.test.ts b/packages/cellix/archunit-tests/src/serenity.contract.test.ts index bc9715fdc..1c52f1177 100644 --- a/packages/cellix/archunit-tests/src/serenity.contract.test.ts +++ b/packages/cellix/archunit-tests/src/serenity.contract.test.ts @@ -22,7 +22,7 @@ async function suiteFixture(): Promise { await write( suiteRoot, 'world.ts', - `import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber';\nimport { SerenityCast } from '@cellix/serenity-framework/serenity';\nimport { infrastructure } from './infrastructure.ts';\nexport const TestWorld = registerManagedSerenityWorld({ infrastructure, createCast: () => new SerenityCast({ useNotepad: true }) });\nexport type TestWorld = InstanceType;\nregisterLifecycleHooks();`, + `import { registerManagedSerenityWorld } from '@cellix/serenity-framework/cucumber';\nimport { SerenityCast } from '@cellix/serenity-framework/serenity';\nimport { infrastructure } from './infrastructure.ts';\nexport const TestWorld = registerManagedSerenityWorld({ infrastructure, createCast: () => new SerenityCast({ useNotepad: true }) });\nexport type TestWorld = InstanceType;`, ); await write( suiteRoot, @@ -64,8 +64,7 @@ describe('Serenity convention public contract', () => { `import { registerManagedSerenityWorld as registerWorld } from '@cellix/serenity-framework/cucumber'; import { SerenityCast as TestCast } from '@cellix/serenity-framework/serenity'; export const TestWorld = registerWorld({ createCast: () => new TestCast({ useNotepad: true }) }); -export type TestWorld = InstanceType; -registerLifecycleHooks();`, +export type TestWorld = InstanceType;`, ); await write( suiteRoot, From 5585aa2c9583c7cb8acc0b1542540e2f8ef2921f Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 6 Jul 2026 15:24:57 -0400 Subject: [PATCH 06/10] arch unit test adjustments --- packages/cellix/archunit-tests/manifest.md | 1 + packages/cellix/archunit-tests/readme.md | 4 +- .../application-composition.contract.test.ts | 50 ++++++++++ .../src/checks/typescript-source.ts | 99 ++++++++++++++++++- .../src/checks/ui-app-composition.ts | 6 +- pnpm-lock.yaml | 10 +- pnpm-workspace.yaml | 2 +- 7 files changed, 158 insertions(+), 14 deletions(-) diff --git a/packages/cellix/archunit-tests/manifest.md b/packages/cellix/archunit-tests/manifest.md index 3a4feae23..54ceaa40b 100644 --- a/packages/cellix/archunit-tests/manifest.md +++ b/packages/cellix/archunit-tests/manifest.md @@ -16,6 +16,7 @@ Each focused export module exposes configuration types, asynchronous checkers th - A checker reports violations without loading consumer runtime modules. - A suite helper lets each consumer own and run its architecture contract. +- UI composition checks require configured providers to occur on the rendered component path to `App` and missing-root guards to terminate the invalid path. - Configuration makes project paths and providers explicit, while Serenity bounded contexts are discovered from the consumer's standard context directory instead of embedding OCom-specific names in reusable rules. ## Intended consumers diff --git a/packages/cellix/archunit-tests/readme.md b/packages/cellix/archunit-tests/readme.md index 712478b74..db286f870 100644 --- a/packages/cellix/archunit-tests/readme.md +++ b/packages/cellix/archunit-tests/readme.md @@ -16,7 +16,7 @@ Current rules in this package include: - circular dependency checks across Cellix package source graphs - framework boundary checks, such as `ui-core` not depending on OCom UI code or app code - API startup composition through infrastructure, context, application services, handlers, and startup, including service initialization during Azure `appStart` -- UI application bootstrap composition, including root validation, configured providers, and React Router +- UI application bootstrap composition, including terminating missing-root validation, configured providers on the rendered path to `App`, and React Router - Serenity/Cucumber managed-world initialization, concrete finalized server infrastructure, lifecycle cleanup, context loaders, and Screenplay delegation ## Application Composition @@ -29,7 +29,7 @@ import { describeApiCompositionTests } from '@cellix/archunit-tests/api'; describeApiCompositionTests({ apiIndexPath: 'src/index.ts' }); ``` -UI applications configure the providers their bootstrap is expected to compose: +UI applications configure the providers their bootstrap is expected to compose around the rendered `App`. Providers may be nested through local wrapper components, but rendering one elsewhere does not satisfy the contract: ```typescript import { describeUiAppCompositionTests } from '@cellix/archunit-tests/ui-app'; diff --git a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts index fcc5b24cd..0fe6c9967 100644 --- a/packages/cellix/archunit-tests/src/application-composition.contract.test.ts +++ b/packages/cellix/archunit-tests/src/application-composition.contract.test.ts @@ -84,6 +84,24 @@ Application.initializeInfrastructureServices( expect(violations).toEqual(expect.arrayContaining([expect.stringContaining('initializeInfrastructureServices'), expect.stringContaining('initializeApplicationServices'), expect.stringContaining('registerAzureFunction')])); }); + it('reports a complete API startup chain composed in reverse order', async () => { + const apiIndexPath = await fixture( + 'index.ts', + `import { Cellix } from './cellix.ts'; +Cellix.startUp() + .registerAzureFunctionHttpHandler('graphql', {}, handler) + .initializeApplicationServices((context) => build(context)) + .setContext(() => ({})) + .initializeInfrastructureServices((registry) => registry.registerInfrastructureService(service));`, + ); + await writeFile( + path.join(path.dirname(apiIndexPath), 'cellix.ts'), + `export class Cellix { private setupLifecycle() { app.hook.appStart(async () => { await this.startAllServicesWithTracing(); this.contextInternal = this.contextCreatorInternal(this); }); } }`, + ); + + expect(await checkApiComposition({ apiIndexPath })).toEqual(expect.arrayContaining([expect.stringContaining('must be composed after')])); + }); + it('reports a Cellix implementation that does not initialize infrastructure during app startup', async () => { const apiIndexPath = await fixture( 'index.ts', @@ -168,6 +186,38 @@ createRoot(mount).render();`, expect(await checkUiAppComposition({ appRoot })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must guard against a missing root element`); }); + it('does not mistake work performed when the mount point exists for a missing-root guard', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (mount) console.info('mount found'); +createRoot(mount).render();`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must guard against a missing root element`); + }); + + it('reports a required provider that does not wrap the rendered app', async () => { + const appRoot = path.dirname( + await fixture( + 'src/main.tsx', + `import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +const mount = document.getElementById('root'); +if (!mount) throw new Error('missing root'); +createRoot(mount).render(<>);`, + ), + ); + await writeFile(path.join(appRoot, 'App.tsx'), `import { Routes, Route } from 'react-router-dom';\nexport default () => } />;`); + + expect(await checkUiAppComposition({ appRoot, requiredProviders: ['ThemeProvider'] })).toContain(`[${path.join(appRoot, 'main.tsx')}] UI bootstrap must compose ThemeProvider around App`); + }); + it('reports a UI entrypoint that skips root validation, providers, or routing', async () => { const appRoot = path.dirname(await fixture('src/main.tsx', `import { createRoot } from 'react-dom/client';\ncreateRoot(document.getElementById('root')).render();`)); await writeFile(path.join(appRoot, 'App.tsx'), 'export default () =>
;'); diff --git a/packages/cellix/archunit-tests/src/checks/typescript-source.ts b/packages/cellix/archunit-tests/src/checks/typescript-source.ts index 5e9753f17..8bceb264c 100644 --- a/packages/cellix/archunit-tests/src/checks/typescript-source.ts +++ b/packages/cellix/archunit-tests/src/checks/typescript-source.ts @@ -50,7 +50,10 @@ export function callPositions(source: ts.SourceFile, names: readonly string[]): if (!ts.isCallExpression(node)) return false; const name = expressionName(node.expression); const matchedName = name && names.find((candidate) => name === candidate || (candidate === 'registerAzureFunction' && name.startsWith(candidate))); - if (matchedName && !positions.has(matchedName)) positions.set(matchedName, node.getStart(source)); + if (matchedName && !positions.has(matchedName)) { + const positionNode = ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression; + positions.set(matchedName, positionNode.getStart(source)); + } return false; }); return positions; @@ -88,6 +91,41 @@ export function containsJsxTag(source: ts.SourceFile, tagName: string): boolean }); } +/** Check whether a JSX component is on a rendered path between the React root and App. */ +export function containsRenderedJsxWrapper(source: ts.SourceFile, wrapperName: string, childName = 'App'): boolean { + const root = '__render_root__'; + const edges = new Map>(); + const connect = (parent: string, child: string): void => { + const children = edges.get(parent) ?? new Set(); + children.add(child); + edges.set(parent, children); + }; + + someNode(source, (node) => { + const tag = jsxTagName(node, source); + if (!tag) return false; + + const parentTag = nearestJsxParentTag(node, source); + if (parentTag) { + connect(parentTag, tag); + } else { + const component = enclosingComponentName(node.parent); + if (component) connect(component, tag); + } + return false; + }); + + someNode(source, (node) => { + if (!ts.isCallExpression(node) || expressionName(node.expression) !== 'render') return false; + for (const argument of node.arguments) { + for (const tag of topLevelJsxTags(argument, source)) connect(root, tag); + } + return false; + }); + + return isReachable(edges, root, wrapperName) && isReachable(edges, wrapperName, childName); +} + /** Check whether a resolved DOM mount point is protected by an explicit guard. */ export function containsMountPointGuard(source: ts.SourceFile, elementId: string): boolean { const mountNames = new Set(); @@ -100,14 +138,69 @@ export function containsMountPointGuard(source: ts.SourceFile, elementId: string }); return someNode(source, (node) => { - if (ts.isIfStatement(node) && referencesAnyIdentifier(node.expression, mountNames)) { - return someNode(node.thenStatement, (child) => ts.isThrowStatement(child) || ts.isCallExpression(child)); + if (ts.isIfStatement(node) && checksMissingValue(node.expression, mountNames)) { + return someNode(node.thenStatement, (child) => ts.isThrowStatement(child) || ts.isReturnStatement(child)); } if (!ts.isCallExpression(node) || !/^(assert|ensure|guard|invariant|require|validate)/i.test(expressionName(node.expression) ?? '')) return false; return node.arguments.some((argument) => referencesAnyIdentifier(argument, mountNames) || isElementLookup(argument, elementId)); }); } +function checksMissingValue(node: ts.Expression, names: ReadonlySet): boolean { + if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) { + return referencesAnyIdentifier(node.operand, names); + } + if (!ts.isBinaryExpression(node)) return false; + if (![ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(node.operatorToken.kind)) return false; + return (referencesAnyIdentifier(node.left, names) && isNullish(node.right)) || (isNullish(node.left) && referencesAnyIdentifier(node.right, names)); +} + +function isNullish(node: ts.Expression): boolean { + return node.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(node) && node.text === 'undefined'); +} + +function jsxTagName(node: ts.Node, source: ts.SourceFile): string | undefined { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) return node.tagName.getText(source); + return undefined; +} + +function nearestJsxParentTag(node: ts.Node | undefined, source: ts.SourceFile): string | undefined { + for (let current = node?.parent; current; current = current.parent) { + if (ts.isJsxElement(current) && current.openingElement !== node) return current.openingElement.tagName.getText(source); + if (ts.isFunctionLike(current)) return undefined; + } + return undefined; +} + +function enclosingComponentName(node: ts.Node | undefined): string | undefined { + for (let current = node; current; current = current.parent) { + if (ts.isFunctionDeclaration(current) && current.name) return current.name.text; + if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) && ts.isVariableDeclaration(current.parent) && ts.isIdentifier(current.parent.name)) return current.parent.name.text; + } + return undefined; +} + +function topLevelJsxTags(node: ts.Node, source: ts.SourceFile): string[] { + const tags: string[] = []; + const visit = (current: ts.Node): void => { + const tag = jsxTagName(current, source); + if (tag) { + tags.push(tag); + return; + } + current.forEachChild(visit); + }; + visit(node); + return tags; +} + +function isReachable(edges: ReadonlyMap>, from: string, to: string, seen = new Set()): boolean { + if (from === to) return true; + if (seen.has(from)) return false; + seen.add(from); + return [...(edges.get(from) ?? [])].some((child) => isReachable(edges, child, to, seen)); +} + function isElementLookup(node: ts.Node, elementId: string): boolean { return ts.isCallExpression(node) && expressionName(node.expression) === 'getElementById' && node.arguments.some((argument) => ts.isStringLiteral(argument) && argument.text === elementId); } diff --git a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts index 0c399e38e..36a4c8296 100644 --- a/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts +++ b/packages/cellix/archunit-tests/src/checks/ui-app-composition.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { containsCall, containsJsxTag, containsMountPointGuard, localImportName, parseTypeScript } from './typescript-source.js'; +import { containsCall, containsJsxTag, containsMountPointGuard, containsRenderedJsxWrapper, localImportName, parseTypeScript } from './typescript-source.js'; export interface UiAppCompositionConfig { /** Directory containing `main.tsx` and `App.tsx`. */ @@ -57,8 +57,8 @@ export async function checkUiAppComposition(config: UiAppCompositionConfig): Pro violations.push(`[${mainPath}] UI bootstrap must render App`); } for (const provider of config.requiredProviders ?? []) { - if (!containsJsxTag(source, provider)) { - violations.push(`[${mainPath}] UI bootstrap must compose ${provider}`); + if (!containsRenderedJsxWrapper(source, provider)) { + violations.push(`[${mainPath}] UI bootstrap must compose ${provider} around App`); } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebf9dac91..7d3ba7010 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,7 +151,7 @@ overrides: node-forge@<1.3.2: '>=1.3.2' picomatch: ^4.0.4 webpack: ^5.105.4 - webpack-dev-server: ^5.2.5 + webpack-dev-server: 5.2.6 express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': ^3.4.0 azurite>uuid: ^3.4.0 @@ -13778,8 +13778,8 @@ packages: webpack: optional: true - webpack-dev-server@5.2.5: - resolution: {integrity: sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==} + webpack-dev-server@5.2.6: + resolution: {integrity: sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==} engines: {node: '>= 18.12.0'} hasBin: true peerDependencies: @@ -16298,7 +16298,7 @@ snapshots: update-notifier: 6.0.2 webpack: 5.105.4(esbuild@0.28.1) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.5(webpack@5.105.4(esbuild@0.28.1)) + webpack-dev-server: 5.2.6(webpack@5.105.4(esbuild@0.28.1)) webpack-merge: 6.0.1 transitivePeerDependencies: - '@parcel/css' @@ -27539,7 +27539,7 @@ snapshots: optionalDependencies: webpack: 5.105.4(esbuild@0.28.1) - webpack-dev-server@5.2.5(webpack@5.105.4(esbuild@0.28.1)): + webpack-dev-server@5.2.6(webpack@5.105.4(esbuild@0.28.1)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c4085ba35..5332b4777 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -102,7 +102,7 @@ overrides: node-forge@<1.3.2: '>=1.3.2' picomatch: ^4.0.4 webpack: ^5.105.4 - webpack-dev-server: ^5.2.5 + webpack-dev-server: 5.2.6 express-rate-limit: 8.5.1 '@azure/ms-rest-js>uuid': '^3.4.0' 'azurite>uuid': '^3.4.0' From f52a27a0242cd78526055e03cdf704642bd5cc9a Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Fri, 10 Jul 2026 15:26:10 -0400 Subject: [PATCH 07/10] fix pipeline issue --- packages/ocom/service-otel/package.json | 2 +- pnpm-lock.yaml | 78 +++++++++++-------------- pnpm-workspace.yaml | 1 - 3 files changed, 34 insertions(+), 47 deletions(-) diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index d0393a0e3..783aeadec 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -37,7 +37,7 @@ "@opentelemetry/sdk-metrics": "2.9.0", "@opentelemetry/sdk-node": "0.220.0", "@opentelemetry/sdk-trace-node": "2.9.0", - "@opentelemetry/semantic-conventions": "1.43.0" + "@opentelemetry/semantic-conventions": "1.42.0" }, "devDependencies": { "@cellix/config-typescript": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc2a9ecd4..eb604e51c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2064,7 +2064,7 @@ importers: version: 1.9.0 '@opentelemetry/instrumentation-dataloader': specifier: 0.37.0 - version: 0.37.0(@opentelemetry/api@1.9.0) + version: 0.37.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) '@opentelemetry/instrumentation-graphql': specifier: 0.68.0 version: 0.68.0(@opentelemetry/api@1.9.0) @@ -2087,8 +2087,8 @@ importers: specifier: 2.9.0 version: 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': - specifier: 1.43.0 - version: 1.43.0 + specifier: 1.42.0 + version: 1.42.0 devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -5601,18 +5601,10 @@ packages: resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} - engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.42.0': resolution: {integrity: sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==} engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.43.0': - resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} - engines: {node: '>=14'} - '@oxc-parser/binding-android-arm-eabi@0.121.0': resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -14551,7 +14543,7 @@ snapshots: '@apollo/utils.withrequired': 3.0.0 '@graphql-tools/schema': 10.0.30(graphql@16.12.0) async-retry: 1.3.3 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-type: 1.0.5 cors: 2.8.5 finalhandler: 2.1.1(supports-color@8.1.1) @@ -14851,7 +14843,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -17980,22 +17972,22 @@ snapshots: '@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/exporter-logs-otlp-grpc@0.220.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18060,7 +18052,7 @@ snapshots: '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/exporter-trace-otlp-grpc@0.220.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18095,19 +18087,19 @@ snapshots: '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/instrumentation-dataloader@0.37.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-dataloader@0.37.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-graphql@0.68.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18115,8 +18107,8 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) + '@opentelemetry/semantic-conventions': 1.42.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color @@ -18125,8 +18117,8 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) + '@opentelemetry/semantic-conventions': 1.42.0 transitivePeerDependencies: - supports-color @@ -18141,12 +18133,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.220.0 import-in-the-middle: 3.3.1 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18206,25 +18198,25 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18239,7 +18231,7 @@ snapshots: '@opentelemetry/api-logs': 0.220.0 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18271,7 +18263,7 @@ snapshots: '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-zipkin': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3': 2.9.0(@opentelemetry/api@1.9.0) @@ -18282,7 +18274,7 @@ snapshots: '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 transitivePeerDependencies: - supports-color @@ -18298,7 +18290,7 @@ snapshots: '@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/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18306,7 +18298,7 @@ snapshots: '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18326,16 +18318,12 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.42.0 '@opentelemetry/semantic-conventions@1.28.0': {} - '@opentelemetry/semantic-conventions@1.38.0': {} - '@opentelemetry/semantic-conventions@1.42.0': {} - '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true @@ -20792,7 +20780,7 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.3.0: + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 2.0.0 @@ -26237,7 +26225,7 @@ snapshots: transitivePeerDependencies: - supports-color - require-in-the-middle@8.0.1: + require-in-the-middle@8.0.1(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 @@ -27833,7 +27821,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.20.0 - es-module-lexer: 2.0.0 + es-module-lexer: 2.3.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3e587a0fc..eceab9db3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -146,4 +146,3 @@ patchedDependencies: minimumReleaseAgeExclude: - fast-uri@3.1.3 || 4.0.1 - - '@opentelemetry/semantic-conventions@1.43.0' From de0a55a3184c4276be97070ed57f56b8e9abdbdf Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Mon, 13 Jul 2026 15:01:49 -0400 Subject: [PATCH 08/10] refresh readme --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 4becc5250..c27b3ef64 100644 --- a/readme.md +++ b/readme.md @@ -217,6 +217,7 @@ First-time setup: configure TLS trust for portless local domains. See ADR 0028 ## Turborepo Optimization + This monorepo uses [Turborepo](https://turbo.build/) for optimized builds and testing: - **Selective Builds**: Only affected packages are built/tested in CI From df6a47b92d28827fa048d17399e16afbb9bfbd10 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Tue, 14 Jul 2026 14:32:59 -0400 Subject: [PATCH 09/10] remove uneeded file canges --- .snyk | 13 +- packages/ocom/service-otel/package.json | 22 +- packages/ocom/service-otel/src/index.test.ts | 10 +- packages/ocom/service-otel/src/index.ts | 4 +- .../ocom/service-otel/src/otel-builder.ts | 17 +- pnpm-lock.yaml | 683 ++++++++---------- pnpm-workspace.yaml | 5 + 7 files changed, 321 insertions(+), 433 deletions(-) diff --git a/.snyk b/.snyk index fd9fba374..c74c63314 100644 --- a/.snyk +++ b/.snyk @@ -98,8 +98,8 @@ ignore: created: '2026-06-08T00:00:00.000Z' 'SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050': - '* > @opentelemetry/exporter-prometheus@0.57.2': - 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' + reason: 'Requires upgrade of @opentelemetry/sdk-node to 0.217.0, which has type errors that break compilation. The OTEL 2.x migration was attempted and reverted (it introduced unrelated churn and broke the coverage gate); staying on the 1.x line for now, so this ignore must persist until the migration is properly scoped into its own change.' + expires: '2026-10-01T00:00:00.000Z' created: '2026-06-01T10:00:00.000Z' 'SNYK-JS-POSTCSSSELECTORPARSER-16873882': - '* > postcss-selector-parser': @@ -170,6 +170,11 @@ ignore: created: '2026-06-18T00:00:00.000Z' 'SNYK-JS-OPENTELEMETRYCORE-17373280': - '* > @opentelemetry/core': - 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' + 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. The OTEL 2.x migration was attempted and reverted, so this remains accepted until the migration lands as its own properly scoped change.' + expires: '2026-10-01T00:00:00.000Z' created: '2026-06-18T00:00:00.000Z' + 'SNYK-JS-OPENTELEMETRYPROPAGATORJAEGER-17901201': + - '* > @opentelemetry/propagator-jaeger': + reason: 'Reintroduced by staying on the OpenTelemetry 1.x SDK line: @opentelemetry/sdk-trace-node@1.30.1 pulls @opentelemetry/propagator-jaeger@1.30.1 transitively. Fixed only in 2.9.0, which requires the OTEL 2.x migration that was reverted here (it bundled unrelated dependency churn and broke the new-code coverage gate). The Jaeger propagator is not used by the Azure Functions telemetry stack, so this is not exploitable in current usage. Accepted until the 2.x migration lands as its own scoped change; tracked alongside SNYK-JS-OPENTELEMETRYCORE-17373280 and SNYK-JS-OPENTELEMETRYEXPORTERPROMETHEUS-16758050.' + expires: '2026-10-01T00:00:00.000Z' + created: '2026-07-14T00:00:00.000Z' diff --git a/packages/ocom/service-otel/package.json b/packages/ocom/service-otel/package.json index 783aeadec..18fb61910 100644 --- a/packages/ocom/service-otel/package.json +++ b/packages/ocom/service-otel/package.json @@ -25,19 +25,19 @@ "clean": "rimraf dist" }, "dependencies": { - "@azure/functions-opentelemetry-instrumentation": "0.3.0", - "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.32", + "@azure/functions-opentelemetry-instrumentation": "0.1.0", + "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.31", "@cellix/api-services-spec": "workspace:*", "@opentelemetry/api": "1.9.0", - "@opentelemetry/instrumentation-dataloader": "0.37.0", - "@opentelemetry/instrumentation-graphql": "0.68.0", - "@opentelemetry/instrumentation-http": "0.220.0", - "@opentelemetry/instrumentation-mongoose": "0.66.0", - "@opentelemetry/sdk-logs": "0.220.0", - "@opentelemetry/sdk-metrics": "2.9.0", - "@opentelemetry/sdk-node": "0.220.0", - "@opentelemetry/sdk-trace-node": "2.9.0", - "@opentelemetry/semantic-conventions": "1.42.0" + "@opentelemetry/instrumentation-dataloader": "0.17.0", + "@opentelemetry/instrumentation-graphql": "0.48.0", + "@opentelemetry/instrumentation-http": "0.52.1", + "@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-trace-node": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" }, "devDependencies": { "@cellix/config-typescript": "workspace:*", diff --git a/packages/ocom/service-otel/src/index.test.ts b/packages/ocom/service-otel/src/index.test.ts index bb8845f5a..0b7eed41c 100644 --- a/packages/ocom/service-otel/src/index.test.ts +++ b/packages/ocom/service-otel/src/index.test.ts @@ -6,12 +6,15 @@ import { ServiceOtel } from './index.ts'; const test = { for: describeFeature }; vi.mock('@opentelemetry/sdk-node', () => { - // Mock resource factories and resources namespace + // Mock Resource class and resources namespace class Resource { private attributes: Record; constructor(attrs: Record) { this.attributes = attrs; } + static default() { + return new Resource({ default: true }); + } merge(other: Resource) { return new Resource({ ...this.attributes, ...other.attributes }); } @@ -30,10 +33,7 @@ vi.mock('@opentelemetry/sdk-node', () => { } return { NodeSDK, - resources: { - defaultResource: () => new Resource({ default: true }), - resourceFromAttributes: (attributes: Record) => new Resource(attributes), - }, + resources: { Resource }, }; }); diff --git a/packages/ocom/service-otel/src/index.ts b/packages/ocom/service-otel/src/index.ts index 3425a74c3..4a456eae5 100644 --- a/packages/ocom/service-otel/src/index.ts +++ b/packages/ocom/service-otel/src/index.ts @@ -30,8 +30,8 @@ export class ServiceOtel implements SyncServiceBase { metricReader: metricReader, instrumentations: instrumentations, - resource: opentelemetry.resources.defaultResource().merge( - opentelemetry.resources.resourceFromAttributes({ + resource: opentelemetry.resources.Resource.default().merge( + new opentelemetry.resources.Resource({ [ATTR_SERVICE_NAME]: 'Cellix Demo', [ATTR_SERVICE_VERSION]: '1.0.0', }), diff --git a/packages/ocom/service-otel/src/otel-builder.ts b/packages/ocom/service-otel/src/otel-builder.ts index af034a93f..f21686e57 100644 --- a/packages/ocom/service-otel/src/otel-builder.ts +++ b/packages/ocom/service-otel/src/otel-builder.ts @@ -5,7 +5,6 @@ import { DataloaderInstrumentation } from '@opentelemetry/instrumentation-datalo import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; -import type { LogRecordExporter } from '@opentelemetry/sdk-logs'; import { BatchLogRecordProcessor, ConsoleLogRecordExporter, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs'; import { ConsoleMetricExporter, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; import type { NodeSDKConfiguration } from '@opentelemetry/sdk-node'; @@ -17,14 +16,7 @@ const { AzureFunctionsInstrumentation } = azureOtel; //Need to use destructuring interface Exporters { traceExporter: AzureMonitorTraceExporter | ConsoleSpanExporter; metricExporter: AzureMonitorMetricExporter | ConsoleMetricExporter; - logExporter: LogRecordExporter; -} - -class FlushableAzureMonitorLogExporter extends AzureMonitorLogExporter { - // The 0.220 log SDK requires this hook; the Azure exporter has no separate buffer to flush. - public forceFlush(): Promise { - return Promise.resolve(); - } + logExporter: AzureMonitorLogExporter | ConsoleLogRecordExporter; } export class OtelBuilder { @@ -51,7 +43,7 @@ export class OtelBuilder { //biome-ignore lint:useLiteralKeys process.env['APPLICATIONINSIGHTS_CONNECTION_STRING'], }), - logExporter: new FlushableAzureMonitorLogExporter({ + logExporter: new AzureMonitorLogExporter({ connectionString: //biome-ignore lint:useLiteralKeys process.env['APPLICATIONINSIGHTS_CONNECTION_STRING'], @@ -64,7 +56,7 @@ export class OtelBuilder { if (useSimpleProcessors) { return { spanProcessors: [new SimpleSpanProcessor(exporters.traceExporter)], - logRecordProcessors: [new SimpleLogRecordProcessor({ exporter: exporters.logExporter })], + logRecordProcessors: [new SimpleLogRecordProcessor(exporters.logExporter)], }; } else { const EXPORT_TIMEOUT_MILLIS = 15000; @@ -77,8 +69,7 @@ export class OtelBuilder { }), ], logRecordProcessors: [ - new BatchLogRecordProcessor({ - exporter: exporters.logExporter, + new BatchLogRecordProcessor(exporters.logExporter, { exportTimeoutMillis: EXPORT_TIMEOUT_MILLIS, maxQueueSize: MAX_QUEUE_SIZE, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb604e51c..35c407400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,8 @@ overrides: '@azure/ms-rest-js>xml2js': 0.5.0 tedious>@azure/identity: 4.2.1 +packageExtensionsChecksum: sha256-mDviJarBPcwNNCTUf3T37btBxDGgV1wZ/iUGQfx5OCA= + patchedDependencies: '@azure/functions@4.11.0': 69772ce521bf6df67d814ff4f419f19b5e966a41c4ce80b5938143ad628e5645 @@ -2051,11 +2053,11 @@ importers: packages/ocom/service-otel: dependencies: '@azure/functions-opentelemetry-instrumentation': - specifier: 0.3.0 - version: 0.3.0(@opentelemetry/api@1.9.0) + specifier: 0.1.0 + version: 0.1.0(@opentelemetry/api@1.9.0) '@azure/monitor-opentelemetry-exporter': - specifier: 1.0.0-beta.32 - version: 1.0.0-beta.32 + specifier: 1.0.0-beta.31 + version: 1.0.0-beta.31 '@cellix/api-services-spec': specifier: workspace:* version: link:../../cellix/api-services-spec @@ -2063,32 +2065,32 @@ importers: specifier: 1.9.0 version: 1.9.0 '@opentelemetry/instrumentation-dataloader': - specifier: 0.37.0 - version: 0.37.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) + specifier: 0.17.0 + version: 0.17.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-graphql': - specifier: 0.68.0 - version: 0.68.0(@opentelemetry/api@1.9.0) + specifier: 0.48.0 + version: 0.48.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: 0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.0) + specifier: 0.52.1 + version: 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-mongoose': - specifier: 0.66.0 - version: 0.66.0(@opentelemetry/api@1.9.0) + specifier: 0.47.0 + version: 0.47.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': - specifier: 0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.0) + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': - specifier: 2.9.0 - version: 2.9.0(@opentelemetry/api@1.9.0) + specifier: 1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: 0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.0) + specifier: 0.57.2 + version: 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': - specifier: 2.9.0 - version: 2.9.0(@opentelemetry/api@1.9.0) + specifier: 1.30.1 + version: 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': - specifier: 1.42.0 - version: 1.42.0 + specifier: 1.28.0 + version: 1.28.0 devDependencies: '@cellix/config-typescript': specifier: workspace:* @@ -3267,8 +3269,8 @@ packages: resolution: {integrity: sha512-ncCkHBNQYJa93dBIh+toH0v1iSgCzSo9tr94s6SMBe7DPWREkaWh8cq33A5P4rPSFX1g5W+3SPvIzDr/6/VOWQ==} engines: {node: '>=18.0'} - '@azure/functions-opentelemetry-instrumentation@0.3.0': - resolution: {integrity: sha512-mZkc2fv3Zu+eNf64yLA0C0wftE8lnPQNZUofWk4UglOWsFiR/SarpV2K8GiqjzQLIH0NXj0wrrmHUIykabgRgg==} + '@azure/functions-opentelemetry-instrumentation@0.1.0': + resolution: {integrity: sha512-eRitTbOUDhlzc4o2Q9rjbXiMYa/ep06m2jIkN7HOuLP0aHnjPh3zHXtqji/NyeqT/GfHjCgJr+r8+49s7KER7w==} engines: {node: '>=18.0'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3297,8 +3299,8 @@ packages: resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} engines: {node: '>=20.0.0'} - '@azure/monitor-opentelemetry-exporter@1.0.0-beta.32': - resolution: {integrity: sha512-Tk5Tv8KwHhKCQlXET/7ZLtjBv1Zi4lmPTadKTQ9KCURRJWdt+6hu5ze52Tlp2pVeg3mg+MRQ9vhWvVNXMZAp/A==} + '@azure/monitor-opentelemetry-exporter@1.0.0-beta.31': + resolution: {integrity: sha512-fH/vFMWOHwN/qdJ1SxVtsJ9zWIGWJdy/tZiVVPsFbpbR4NT49J5Im72iCcTyvABRPfIkMz/zsD58Yilf0yiEZw==} engines: {node: '>=18.0.0'} '@azure/ms-rest-js@1.11.2': @@ -5305,31 +5307,27 @@ packages: resolution: {integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.209.0': - resolution: {integrity: sha512-xomnUNi7TiAGtOgs0tb54LyrjRZLu9shJGGwkcN7NgtiPYOpNnKLkRJtzZvTjD/w6knSZH9sFZcUSUovYOPg6A==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.220.0': - resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} - engines: {node: '>=8.0.0'} - '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.220.0': - resolution: {integrity: sha512-glfIVKnZevRin8fY/9uES/mhRtMT1lGINLHc9MIo5fTQZXswEEHamJtgjv4MTtzgnhHGC92mIS/0lzAUZMyE0w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': ^1.9.0 + '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/context-async-hooks@2.9.0': - resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/core@1.25.1': + resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -5339,12 +5337,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.0.0': - resolution: {integrity: sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.2.0': resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5357,98 +5349,92 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.9.0': - resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/exporter-logs-otlp-grpc@0.220.0': - resolution: {integrity: sha512-s0sRPCSlXYqlgObOpCftomJllp3LfUL9FobQ5csg2172ydVhSEnu1ptpsVBJadazs5nUNp7vDuLE03FAFWTLOQ==} - 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.220.0': - resolution: {integrity: sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==} - 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.220.0': - resolution: {integrity: sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==} - 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.220.0': - resolution: {integrity: sha512-U128izvJfX/dW9jRGP0gIfadR1Hg7ft3UEGIeRxLFK70m2BWw6AtNCOnsUygpw2zCgR/ygdWbGpcL6TmhW0ZGw==} - 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.220.0': - resolution: {integrity: sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==} - 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.220.0': - resolution: {integrity: sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==} - 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 - '@opentelemetry/exporter-prometheus@0.220.0': - resolution: {integrity: sha512-JZD5DL/NBpVd2BHefvYosm3G40UZ/KzExLv5tc0eZe0CtrsHHtcOk3YPUxR2EINmUeBf8+w5UReTV8fFPn95lA==} + '@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.220.0': - resolution: {integrity: sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==} - 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.220.0': - resolution: {integrity: sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==} - 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.220.0': - resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} - 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.9.0': - resolution: {integrity: sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==} - 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 - '@opentelemetry/instrumentation-dataloader@0.37.0': - resolution: {integrity: sha512-9w0yRC6nyYYQkxwf3vOEBfxiGjyJaQDhOjpusZFmgOxW2bArtSrV8t2hdeLhU6dXy1Kn/N+yocnhWin2B/xEWQ==} + '@opentelemetry/instrumentation-dataloader@0.17.0': + resolution: {integrity: sha512-JqovxOo7a65+3A/W+eiqUv7DrDsSvsY0NemHJ4uyVrzD4bpDYofVRdnz/ehYcNerlxVIKU+HcybDmiaoj41DPw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.68.0': - resolution: {integrity: sha512-ZpL6FYk6NZTuBO5Dh8G7SNBANswTIxCI5qod2pQjF3fsKpxDRHA4FJ6yYK3TdJhFLloMdyRVmE1gMCxG7q6hYQ==} + '@opentelemetry/instrumentation-graphql@0.48.0': + resolution: {integrity: sha512-w1sbf9F9bQTpIWGnKWhH1A+9N9rKxS4eM+AzczgMWp272ZM9lQv4zLTrH5NRST2ltY3nmZ72wkfFrSR0rECi0g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.220.0': - resolution: {integrity: sha512-Szt4dO2Boz2CDr38DaSw/lnqwhwKl+IAdgNGEGgSm2Anb+fwPtIAGmIwkhsLLN69QQZQE96JxjMKYY4rlRkYKw==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/instrumentation-http@0.52.1': + resolution: {integrity: sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.66.0': - resolution: {integrity: sha512-CxzRijJCexHnucPDuKSz1PTJf6+t0VJxFDNQtoCwSPo8eGXKoEuJ/I4V2kMQjMbcY4qISN3Efa+7TL5Zy6zqTA==} + '@opentelemetry/instrumentation-mongoose@0.47.0': + resolution: {integrity: sha512-zg4ixMNmuACda75eOFa1m5h794zC9wp397stX0LAZvOylSb6dWT52P6ElkVQMV42C/27liEdQWxpabsamB+XPQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -5459,45 +5445,45 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.220.0': - resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} - 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.220.0': - resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} - 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.220.0': - resolution: {integrity: sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==} - 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.220.0': - resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} - 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 - '@opentelemetry/propagator-b3@2.9.0': - resolution: {integrity: sha512-WrOT1WsOUG+B7hstD2RYoMPIOK76G8E9AQHhMjUvrQaGx/oA7rPWQvvr1Rqv7+yy4R0ZMVwWLC4vW2xnkgWPAQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.9.0': - resolution: {integrity: sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==} - engines: {node: ^18.19.0 || >=20.6.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' @@ -5507,12 +5493,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/resources@2.0.0': - resolution: {integrity: sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/resources@2.2.0': resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} engines: {node: ^18.19.0 || >=20.6.0} @@ -5525,23 +5505,17 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/resources@2.9.0': - resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-logs@0.200.0': - resolution: {integrity: sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-logs@0.220.0': - resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/api': '>=1.3.0 <1.10.0' '@opentelemetry/sdk-metrics@2.7.1': resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} @@ -5549,15 +5523,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.9.0': - resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.9.0 <1.10.0' - - '@opentelemetry/sdk-node@0.220.0': - resolution: {integrity: sha512-wHtGyHhSKHNH3fym33xRu4Ef/HXTFvX8eQ42xdQdEO9LYx9Y2qNyBDJytyqVlvmo6abWZlNYTUthuAGUMYqYnQ==} - 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' @@ -5573,15 +5541,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.9.0': - resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' - - '@opentelemetry/sdk-trace-node@2.9.0': - resolution: {integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==} - engines: {node: ^18.19.0 || >=20.6.0} + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -5591,11 +5553,9 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace@2.9.0': - resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.25.1': + resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} + engines: {node: '>=14'} '@opentelemetry/semantic-conventions@1.28.0': resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} @@ -8243,9 +8203,6 @@ 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==} @@ -9369,9 +9326,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -9886,10 +9840,6 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} - import-in-the-middle@3.3.1: - resolution: {integrity: sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==} - engines: {node: '>=18'} - import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -12503,10 +12453,6 @@ 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==} @@ -14745,10 +14691,10 @@ snapshots: '@azure/functions-extensions-base@0.2.0': {} - '@azure/functions-opentelemetry-instrumentation@0.3.0(@opentelemetry/api@1.9.0)': + '@azure/functions-opentelemetry-instrumentation@0.1.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.209.0 + '@opentelemetry/api-logs': 0.57.2 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -14831,18 +14777,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/monitor-opentelemetry-exporter@1.0.0-beta.32': + '@azure/monitor-opentelemetry-exporter@1.0.0-beta.31': dependencies: '@azure/core-auth': 1.10.1 '@azure/core-client': 1.10.1 '@azure/core-rest-pipeline': 1.22.2 '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.200.0 - '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@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/semantic-conventions': 1.42.0 tslib: 2.8.1 transitivePeerDependencies: @@ -17940,40 +17886,30 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.209.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.220.0': + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs@0.52.1': + '@opentelemetry/api-logs@0.57.2': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/configuration@0.220.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - yaml: 2.8.3 - '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.25.1 '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -17984,141 +17920,142 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.42.0 - - '@opentelemetry/exporter-logs-otlp-grpc@0.220.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.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.220.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.220.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.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.220.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.220.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/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.220.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/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.220.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.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.0(@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.220.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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.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/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.220.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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.0(@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.220.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.8.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/semantic-conventions': 1.42.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.220.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.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.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/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.220.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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.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/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.220.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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.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/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.9.0(@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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.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.37.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1)': + '@opentelemetry/instrumentation-dataloader@0.17.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) + '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.68.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-graphql@0.48.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) + '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-http@0.220.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) - '@opentelemetry/semantic-conventions': 1.42.0 - forwarded-parse: 2.1.2 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + semver: 7.8.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.66.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongoose@0.47.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) - '@opentelemetry/semantic-conventions': 1.42.0 + '@opentelemetry/instrumentation': 0.200.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 transitivePeerDependencies: - supports-color @@ -18133,19 +18070,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1)': + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.220.0 - import-in-the-middle: 3.3.1 - require-in-the-middle: 8.0.1(supports-color@8.1.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.8.0 + 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 @@ -18154,39 +18094,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.220.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.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.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.220.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.4 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.220.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.220.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.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.0(@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.6.5 - '@opentelemetry/propagator-b3@2.9.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.9.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@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)': dependencies: @@ -18194,12 +18135,6 @@ snapshots: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.28.0 - '@opentelemetry/resources@2.0.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -18212,26 +18147,18 @@ snapshots: '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 - - '@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.200.0 - '@opentelemetry/core': 2.0.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.0.0(@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.220.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.0)': dependencies: @@ -18239,42 +18166,29 @@ snapshots: '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - - '@opentelemetry/sdk-node@0.220.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/configuration': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.0)(supports-color@8.1.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.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.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 @@ -18292,20 +18206,15 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.42.0 - '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 - - '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.8.0 '@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0)': dependencies: @@ -18313,12 +18222,7 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 + '@opentelemetry/semantic-conventions@1.25.1': {} '@opentelemetry/semantic-conventions@1.28.0': {} @@ -20504,7 +20408,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.42.0 + '@opentelemetry/semantic-conventions': 1.28.0 cls-hooked: 4.2.2 continuation-local-storage: 3.2.1 diagnostic-channel: 1.1.1 @@ -21124,7 +21028,7 @@ snapshots: chromatic@16.10.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 chrome-devtools-mcp@1.3.0: {} @@ -21140,8 +21044,6 @@ snapshots: cjs-module-lexer@1.4.3: {} - cjs-module-lexer@2.2.0: {} - class-transformer@0.5.1: {} classnames@2.5.1: {} @@ -21469,7 +21371,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.10) postcss-modules-values: 4.0.0(postcss@8.5.10) postcss-value-parser: 4.2.0 - semver: 7.7.4 + semver: 7.8.0 optionalDependencies: webpack: 5.105.4(esbuild@0.28.1) @@ -22402,8 +22304,6 @@ snapshots: dependencies: fetch-blob: 3.2.0 - forwarded-parse@2.1.2: {} - forwarded@0.2.0: {} fraction.js@5.3.4: {} @@ -22547,7 +22447,7 @@ snapshots: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.7.4 + semver: 7.8.0 serialize-error: 7.0.1 global-dirs@3.0.1: @@ -23060,12 +22960,6 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 - import-in-the-middle@3.3.1: - dependencies: - cjs-module-lexer: 2.2.0 - es-module-lexer: 2.3.0 - module-details-from-path: 1.0.4 - import-lazy@4.0.0: {} imurmurhash@0.1.4: {} @@ -23831,7 +23725,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 map-cache@0.2.2: {} @@ -24462,7 +24356,7 @@ snapshots: https-proxy-agent: 7.0.6 mongodb: 6.21.0 new-find-package-json: 2.0.0(supports-color@8.1.1) - semver: 7.7.4 + semver: 7.8.0 tar-stream: 3.1.7 tslib: 2.8.1 yauzl: 3.2.1 @@ -24488,7 +24382,7 @@ snapshots: https-proxy-agent: 7.0.6 mongodb: 6.21.0 new-find-package-json: 2.0.0(supports-color@8.1.1) - semver: 7.7.4 + semver: 7.8.0 tar-stream: 3.1.7 tslib: 2.8.1 yauzl: 3.2.1 @@ -25390,7 +25284,7 @@ snapshots: cosmiconfig: 8.3.6(typescript@6.0.3) jiti: 2.6.1 postcss: 8.5.10 - semver: 7.7.4 + semver: 7.8.0 webpack: 5.105.4(esbuild@0.28.1) transitivePeerDependencies: - typescript @@ -26225,13 +26119,6 @@ snapshots: transitivePeerDependencies: - supports-color - require-in-the-middle@8.0.1(supports-color@8.1.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: {} @@ -26448,7 +26335,7 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.0 semver@5.7.2: {} @@ -27480,7 +27367,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.4 + semver: 7.8.0 semver-diff: 4.0.0 xdg-basedir: 5.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index eceab9db3..317aa125e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -141,6 +141,11 @@ overrides: '@azure/ms-rest-js>xml2js': 0.5.0 'tedious>@azure/identity': 4.2.1 +packageExtensions: + '@azure/functions-opentelemetry-instrumentation@0.1.0': + dependencies: + '@opentelemetry/api-logs': 0.57.2 + patchedDependencies: '@azure/functions@4.11.0': patches/@azure__functions@4.11.0.patch From 05d1e8dda3c53bdd445f38d0d68ea7bbacac0be7 Mon Sep 17 00:00:00 2001 From: Jason Morais Date: Wed, 15 Jul 2026 13:25:46 -0400 Subject: [PATCH 10/10] retry build pipeline --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index c27b3ef64..d15f9a91c 100644 --- a/readme.md +++ b/readme.md @@ -218,6 +218,7 @@ First-time setup: configure TLS trust for portless local domains. See ADR 0028 ## Turborepo Optimization + This monorepo uses [Turborepo](https://turbo.build/) for optimized builds and testing: - **Selective Builds**: Only affected packages are built/tested in CI