From dc0541997629f400cd7d71aed5fa54441daf0a77 Mon Sep 17 00:00:00 2001 From: Nicolas Bouliol Date: Fri, 14 Aug 2026 16:14:59 +0200 Subject: [PATCH 1/3] test(agent-bff): drive a generated client through list, count, form and execute Emits the document through the CLI command, runs @hey-api/openapi-ts on it, and drives the generated client against the real data and action middlewares over HTTP, so a 2xx proves the documented paths, fields and operator enums are sufficient to construct the call. The filter operator is read from the emitted enum rather than hardcoded, and a negative control asserts an operator the document leaves out is rejected with 400 - without it a permissive stub would keep the check green. Fixes PRD-687 Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 + packages/agent-bff/package.json | 1 + .../openapi/openapi-generated-client.test.ts | 401 ++++++++++++++++++ yarn.lock | 296 ++++++++++++- 4 files changed, 690 insertions(+), 10 deletions(-) create mode 100644 packages/agent-bff/test/openapi/openapi-generated-client.test.ts diff --git a/.gitignore b/.gitignore index 59ab18a925..97585b0f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ lerna-debug.log # forest-bff openapi --output default destination openapi.json +# document and client written by the OpenAPI codegen verification test +packages/agent-bff/test/openapi/.generated # yarn yarn-error.log diff --git a/packages/agent-bff/package.json b/packages/agent-bff/package.json index 14d0446677..97b3c1ac5c 100644 --- a/packages/agent-bff/package.json +++ b/packages/agent-bff/package.json @@ -41,6 +41,7 @@ "zod": "4.3.6" }, "devDependencies": { + "@hey-api/openapi-ts": "0.99.0", "@redocly/cli": "2.35.1", "@types/jsonwebtoken": "^9.0.1", "@types/koa": "^2.13.5", diff --git a/packages/agent-bff/test/openapi/openapi-generated-client.test.ts b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts new file mode 100644 index 0000000000..c18e64a65c --- /dev/null +++ b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts @@ -0,0 +1,401 @@ +import type { Action, AgentActionClient } from '../../src/action/agent-action-client'; +import type { AgentDataClient } from '../../src/data/agent-data-client'; +import type { Logger } from '../../src/ports/logger-port'; +import type ReadModelStore from '../../src/read-model/read-model-store'; +import type { Server } from 'http'; + +import { bodyParser } from '@koa/bodyparser'; +import { spawnSync } from 'child_process'; +import { readFileSync, rmSync } from 'fs'; +import Koa from 'koa'; +import path from 'path'; + +import createActionRoutesMiddleware from '../../src/action/action-routes-middleware'; +import dispatchCli from '../../src/cli-dispatch'; +import createDataRoutesMiddleware from '../../src/data/data-routes-middleware'; +import createErrorMiddleware from '../../src/http/error-middleware'; +import ReadModel from '../../src/read-model/read-model'; +import createTimezoneMiddleware, { TIMEZONE_HEADER } from '../../src/timezone/timezone-middleware'; +import { action, collection, column, relation } from '../read-model/fixtures'; + +const MARK_AS_PAID = 'Mark as paid'; +const GENERATE_INVOICE = 'Générer la facture'; + +// One fixture for both sides: the document is generated from this schema and these capabilities, and +// the chain the generated client calls runs on the very same ones. Two sources would let the test go +// green while the document described a surface the runtime does not have. +const SCHEMA = [ + collection( + 'users', + [column('id'), column('email'), relation('orders', 'HasMany', 'orders.userId')], + [ + action(MARK_AS_PAID, '/forest/users/actions/mark-as-paid'), + action(GENERATE_INVOICE, '/forest/users/actions/generer-la-facture'), + ], + ), + collection('orders', [column('id')]), +]; + +// Deliberately a single operator: it makes the negative control below unambiguous, since any other +// operator is then out of the documented set without being unmappable (which would answer 500). +const CAPABILITIES = { fields: [{ name: 'id', type: 'Number', operators: ['equal'] }] }; + +const DOCUMENTED_OPERATOR = 'Equal'; +const UNDOCUMENTED_OPERATOR = 'GreaterThan'; + +const fetchSchema = jest.fn().mockResolvedValue(SCHEMA); +const fetchCapabilities = jest.fn().mockResolvedValue(CAPABILITIES); + +jest.mock('../../src/read-model/forest-schema-client', () => ({ + __esModule: true, + default: class { + // eslint-disable-next-line class-methods-use-this + fetchSchema() { + return fetchSchema(); + } + }, +})); + +jest.mock('../../src/read-model/agent-capabilities-fetcher', () => ({ + __esModule: true, + default: () => fetchCapabilities, +})); + +const ENV = { + FOREST_AUTH_SECRET: 'auth-secret', + FOREST_ENV_SECRET: 'env-secret', + FOREST_SERVER_URL: 'https://api.forestadmin.com', + AGENT_URL: 'https://agent.example.com', + HTTP_PORT: '0', +} satisfies NodeJS.ProcessEnv; + +const TIMEZONE = 'Europe/Paris'; +const API_KEY = 'fixture-bff-key'; +const RECORD_ID = '1'; + +const GENERATED_DIR = path.join(__dirname, '.generated'); +const DOCUMENT_FILE = path.join(GENERATED_DIR, 'openapi.json'); +const CLIENT_DIR = path.join(GENERATED_DIR, 'client'); + +const CODEGEN_BIN = path.join( + path.dirname(require.resolve('@hey-api/openapi-ts/package.json')), + 'bin/run.js', +); + +const noopLogger: Logger = () => undefined; + +interface CallResult { + response: { status: number }; + data?: unknown; + error?: unknown; +} + +type Call = (options?: Record) => Promise; + +interface GeneratedClient { + setConfig: (config: Record) => unknown; +} + +const OPERATIONS = [ + 'listRecordsUsers', + 'countRecordsUsers', + 'listRecordsOrders', + 'countRecordsOrders', + 'listRelatedRecordsUsersOrders', + 'countRelatedRecordsUsersOrders', + 'getActionFormUsersMarkAsPaid', + 'executeActionUsersMarkAsPaid', + 'getActionFormUsersGNRerLaFacture', + 'executeActionUsersGNRerLaFacture', +] as const; + +const dataClient: AgentDataClient = { + list: async () => [{ id: 1, email: 'someone@example.com' }], + countRaw: async () => ({ count: 1 }), + listRelation: async () => [{ id: 7 }], + countRelationRaw: async () => ({ count: 1 }), +}; + +function makeAction(): Action { + return { + tryToSetFields: async () => [], + setFields: async () => undefined, + execute: async () => ({ success: 'Done' }), + getFields: () => [ + { + getName: () => 'comment', + getType: () => 'String', + getValue: () => null, + isRequired: () => false, + }, + ], + getEnumField: () => ({ getOptions: () => undefined }), + getLayout: () => ({ layout: [] }), + }; +} + +const actionClient: AgentActionClient = { loadAction: async () => makeAction() }; + +function storeOf(readModel: ReadModel): ReadModelStore { + return { + getReadModel: async () => readModel, + getCapabilities: async () => ({ capabilities: CAPABILITIES, readModel }), + } as unknown as ReadModelStore; +} + +const receivedKeys: string[] = []; + +function buildApp(): Koa { + const store = storeOf(new ReadModel(SCHEMA)); + const app = new Koa(); + app.silent = true; + app.use(createErrorMiddleware({ logger: noopLogger })); + app.use(bodyParser()); + app.use(async (ctx, next) => { + receivedKeys.push(ctx.get('X-Forest-Bff-Key')); + // Auth is out of this test's scope (the gate is its own ticket): the request arrives with agent + // credentials already resolved, exactly as the auth chain would have left it. + ctx.state.agentToken = 'agent-jwt'; + await next(); + }); + app.use(createTimezoneMiddleware({ defaultTimezone: TIMEZONE })); + app.use( + createDataRoutesMiddleware({ + store, + agentUrl: ENV.AGENT_URL, + logger: noopLogger, + createClient: () => dataClient, + }), + ); + app.use( + createActionRoutesMiddleware({ + store, + agentUrl: ENV.AGENT_URL, + logger: noopLogger, + createClient: () => actionClient, + }), + ); + + return app; +} + +function documentedOperators(document: { + components: { schemas: Record }; +}): string[] { + const leaf = document.components.schemas['FilterLeaf_users-1']; + + return leaf?.properties?.operator?.enum ?? []; +} + +describe('a client generated from the emitted OpenAPI document', () => { + let codegen: { status: number | null; output: string }; + let document: ReturnType; + let sdk: Record; + let server: Server; + + beforeAll(async () => { + rmSync(GENERATED_DIR, { recursive: true, force: true }); + + const stderr = jest.spyOn(process.stderr, 'write').mockReturnValue(true); + const emitted = await dispatchCli(['openapi', '--output', DOCUMENT_FILE], ENV, noopLogger); + stderr.mockRestore(); + + if (emitted.exitCode !== 0) throw new Error('The CLI could not emit the document'); + + const result = spawnSync( + process.execPath, + [CODEGEN_BIN, '--input', DOCUMENT_FILE, '--output', CLIENT_DIR, '--silent'], + { encoding: 'utf8' }, + ); + codegen = { + status: result.status, + output: `${result.stdout ?? ''}${result.stderr ?? ''}`, + }; + + document = JSON.parse(readFileSync(DOCUMENT_FILE, 'utf8')); + + // eslint-disable-next-line global-require, import/no-dynamic-require + sdk = require(path.join(CLIENT_DIR, 'sdk.gen.ts')); + // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires + const { client } = require(path.join(CLIENT_DIR, 'client.gen.ts')) as { + client: GeneratedClient; + }; + + server = buildApp().listen(0); + const { port } = server.address() as { port: number }; + + client.setConfig({ + baseUrl: `http://127.0.0.1:${port}`, + auth: () => API_KEY, + headers: { [TIMEZONE_HEADER]: TIMEZONE }, + }); + }, 60_000); + + afterAll(() => { + server?.close(); + rmSync(GENERATED_DIR, { recursive: true, force: true }); + }); + + beforeEach(() => { + receivedKeys.length = 0; + }); + + describe('when a standard codegen reads the document', () => { + it('should generate without failing, since a consumer runs this before anything else', () => { + expect(codegen).toEqual({ status: 0, output: '' }); + }); + + it('should expose one function per documented operation, named after its operationId', () => { + expect(OPERATIONS.filter(name => typeof sdk[name] !== 'function')).toEqual([]); + }); + + it('should carry the documented operator set into the generated types', () => { + const types = readFileSync(path.join(CLIENT_DIR, 'types.gen.ts'), 'utf8'); + + expect( + documentedOperators(document).filter(operator => !types.includes(`'${operator}'`)), + ).toEqual([]); + }); + }); + + describe('when the generated client lists and counts a collection', () => { + it('should reach the collection list endpoint', async () => { + const { response, data } = await sdk.listRecordsUsers({ + body: { projection: ['id'], sort: [{ field: 'id', direction: 'asc' }] }, + }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { + data: [ + { + id: 1, + email: 'someone@example.com', + __forest: { collection: 'users', primaryKey: { id: '1' } }, + }, + ], + meta: { countStatus: 'not_requested' }, + }, + }); + }); + + it('should reach the collection count endpoint', async () => { + const { response, data } = await sdk.countRecordsUsers({ body: {} }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { count: 1, countStatus: 'available' }, + }); + }); + + it('should send the credential under the documented security scheme', async () => { + await sdk.countRecordsUsers({ body: {} }); + + expect(receivedKeys).toEqual([API_KEY]); + }); + }); + + describe('when the generated client lists and counts a relation', () => { + it('should reach the relation list endpoint', async () => { + const { response, data } = await sdk.listRelatedRecordsUsersOrders({ + body: { parentId: RECORD_ID, projection: ['id'] }, + }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { + data: [{ id: 7, __forest: { collection: 'orders', primaryKey: { id: '7' } } }], + meta: { countStatus: 'not_requested' }, + }, + }); + }); + + it('should reach the relation count endpoint', async () => { + const { response, data } = await sdk.countRelatedRecordsUsersOrders({ + body: { parentId: RECORD_ID }, + }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { count: 1, countStatus: 'available' }, + }); + }); + }); + + describe('when the generated client drives an action', () => { + it('should load the form', async () => { + const { response, data } = await sdk.getActionFormUsersMarkAsPaid({ + body: { recordIds: [RECORD_ID] }, + }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { + fields: [{ name: 'comment', type: 'String', value: null, isRequired: false }], + canExecute: true, + requiredFields: [], + skippedFields: [], + layout: [], + }, + }); + }); + + it('should execute the action', async () => { + const { response, data } = await sdk.executeActionUsersMarkAsPaid({ + body: { recordIds: [RECORD_ID], values: { comment: 'paid' } }, + }); + + expect({ status: response.status, data }).toEqual({ + status: 200, + data: { type: 'success', message: 'Done', invalidated: [], html: null }, + }); + }); + + it('should reach an action whose name needs URL-encoding beyond spaces', async () => { + const form = await sdk.getActionFormUsersGNRerLaFacture({ body: { recordIds: [RECORD_ID] } }); + const executed = await sdk.executeActionUsersGNRerLaFacture({ + body: { recordIds: [RECORD_ID] }, + }); + + expect([form.response.status, executed.response.status]).toEqual([200, 200]); + }); + }); + + describe("when the generated client filters on a documented field's operator", () => { + it('should document exactly the capabilities operator, normalized', () => { + expect(documentedOperators(document)).toEqual([DOCUMENTED_OPERATOR]); + }); + + it('should be accepted for every operator the document advertises', async () => { + const statuses = await Promise.all( + documentedOperators(document).map(async operator => { + const { response } = await sdk.listRecordsUsers({ + body: { filter: { field: 'id', operator, value: 1 } }, + }); + + return [operator, response.status]; + }), + ); + + expect(statuses).toEqual([[DOCUMENTED_OPERATOR, 200]]); + }); + + it('should be rejected for an operator the document leaves out, so the enum means something', async () => { + const { response, error } = await sdk.listRecordsUsers({ + body: { filter: { field: 'id', operator: UNDOCUMENTED_OPERATOR, value: 1 } }, + }); + + expect({ status: response.status, error }).toEqual({ + status: 400, + error: { + error: { + type: 'invalid_filter_operator', + status: 400, + message: expect.stringContaining('id'), + details: { field: 'id', validOperators: [DOCUMENTED_OPERATOR] }, + }, + }, + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index a38e6baca2..3dfb58df22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1844,6 +1844,67 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@hey-api/codegen-core@0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@hey-api/codegen-core/-/codegen-core-0.9.1.tgz#e66bcc294ee16c4dcb8b8750b70e62d3b53f5fea" + integrity sha512-s97jL1dgTMuiMHv2BZ1X4Tgd99Mf9GOvGdNqNcGwIMmnR+PgYNoraj4Zvp134MKsNCap/m7k0r0vKKnl56pj4w== + dependencies: + "@hey-api/types" "0.1.4" + ansi-colors "4.1.3" + c12 "3.3.4" + color-support "1.1.3" + +"@hey-api/json-schema-ref-parser@1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.4.4.tgz#d5108418ab5af0bffbace5813256248662f32039" + integrity sha512-otmd+zCxbYVBIp/mlMTnGkvlNYLkVKgs3VOIq0kSnenhB1+fRwLPQIeSwyWM6E51oXhUedkYjVsVpkVexeuJOA== + dependencies: + "@jsdevtools/ono" "7.1.3" + "@types/json-schema" "7.0.15" + js-yaml "4.2.0" + +"@hey-api/openapi-ts@0.99.0": + version "0.99.0" + resolved "https://registry.yarnpkg.com/@hey-api/openapi-ts/-/openapi-ts-0.99.0.tgz#d55d0636f40490000aa0efbe1b193610fc2c4992" + integrity sha512-SePU/5oEWWkvUBYmvzdYRctseoLuskyhs4ET0RvLIcmzc8yLQoA2R+KtBIQ8bPsoSUB0m4E5SmBnl6aGSA0szQ== + dependencies: + "@hey-api/codegen-core" "0.9.1" + "@hey-api/json-schema-ref-parser" "1.4.4" + "@hey-api/shared" "0.5.0" + "@hey-api/spec-types" "0.2.0" + "@hey-api/types" "0.1.4" + "@lukeed/ms" "2.0.2" + ansi-colors "4.1.3" + color-support "1.1.3" + commander "15.0.0" + get-tsconfig "4.14.0" + +"@hey-api/shared@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@hey-api/shared/-/shared-0.5.0.tgz#89a07225eb15d69b0881a106109158861ea8b46a" + integrity sha512-JN/j4Ebh4cJGYIQ5cwWuqe7GeSUyQoz7oC51WqyhKOcrejK6DKZMDkshc5d1eKTRuRL+rjozuRcoUaZZn2DGPw== + dependencies: + "@hey-api/codegen-core" "0.9.1" + "@hey-api/json-schema-ref-parser" "1.4.4" + "@hey-api/spec-types" "0.2.0" + "@hey-api/types" "0.1.4" + ansi-colors "4.1.3" + cross-spawn "7.0.6" + open "11.0.0" + semver "7.8.4" + +"@hey-api/spec-types@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@hey-api/spec-types/-/spec-types-0.2.0.tgz#5b6dcdd1bdeb978033f0d250654ec4d440e90030" + integrity sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg== + dependencies: + "@hey-api/types" "0.1.4" + +"@hey-api/types@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@hey-api/types/-/types-0.1.4.tgz#d73731c8ffb5d5b898c01288ca7dfbc367b561ea" + integrity sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg== + "@hono/node-server@^1.19.9 || ^2.0.5": version "2.1.1" resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-2.1.1.tgz#9cfa8649e9ecbcd48edf505b103002402ea51e70" @@ -2466,6 +2527,11 @@ resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.3.tgz#41ae1c07de1ebe0f6dde1abcbc9700a09b9c6056" integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== +"@jsdevtools/ono@7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@koa/bodyparser@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@koa/bodyparser/-/bodyparser-6.0.0.tgz#e362ddb3691276064f36e8cbf79b66f5873360a0" @@ -2592,6 +2658,11 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@lukeed/ms@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.2.tgz#07f09e59a74c52f4d88c6db5c1054e819538e2a8" + integrity sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA== + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -4546,7 +4617,7 @@ resolved "https://registry.yarnpkg.com/@types/json-api-serializer/-/json-api-serializer-2.6.6.tgz#26b5381214aa19bb98a6931fe41c3a336fc7f169" integrity sha512-8XVIVyMNoFMz3pfR3tPHnJ9YlgUQDEWvTxajVakmOjSxWekJvmi2GRFbtaREQiOGtffnHImD0jbR80NQtpib9g== -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@7.0.15", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -5938,6 +6009,13 @@ builtins@^1.0.3: resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -5955,6 +6033,24 @@ bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +c12@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/c12/-/c12-3.3.4.tgz#1253a5faf8b61244884d42459b4a6412571fe9f3" + integrity sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA== + dependencies: + chokidar "^5.0.0" + confbox "^0.2.4" + defu "^6.1.6" + dotenv "^17.3.1" + exsolve "^1.0.8" + giget "^3.2.0" + jiti "^2.6.1" + ohash "^2.0.11" + pathe "^2.0.3" + perfect-debounce "^2.1.0" + pkg-types "^2.3.0" + rc9 "^3.0.1" + cacache@^15.2.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" @@ -6208,6 +6304,13 @@ chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" +chokidar@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5" + integrity sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== + dependencies: + readdirp "^5.0.0" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -6516,6 +6619,11 @@ combined-stream@1.0.8, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-15.0.0.tgz#96f3961f12adac1799ef3fbd8bc61d40572d1b11" + integrity sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg== + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -6583,6 +6691,11 @@ concurrently@^9.0.0: tree-kill "1.2.2" yargs "17.7.2" +confbox@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.4.tgz#592e7be71f882a4a874e3c88f0ac1ef6f7da1ce5" + integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ== + config-chain@^1.1.11: version "1.1.13" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -6876,6 +6989,15 @@ cross-fetch@^4.0.0: dependencies: node-fetch "^2.7.0" +cross-spawn@7.0.6, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^6.0.0: version "6.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" @@ -6887,15 +7009,6 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -7068,6 +7181,19 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +default-browser-id@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" + integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== + +default-browser@^5.4.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.1.tgz#1790affc52680fbb11e17cab2752d69aa2a37d2c" + integrity sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + defaults@1.0.4, defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -7098,6 +7224,11 @@ define-lazy-prop@2.0.0, define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -7107,6 +7238,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, de has-property-descriptors "^1.0.0" object-keys "^1.1.1" +defu@^6.1.6: + version "6.1.7" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.7.tgz#72543567c8e9f97ff13ce402b6dbe09ac5ae4d23" + integrity sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ== + delayed-stream@1.0.0, delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -7142,6 +7278,11 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +destr@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb" + integrity sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA== + destroy@1.2.0, destroy@^1.0.4, destroy@^1.2.0, destroy@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -7285,6 +7426,11 @@ dotenv@^16.4.5: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.5.0.tgz#092b49f25f808f020050051d1ff258e404c78692" integrity sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg== +dotenv@^17.3.1: + version "17.4.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== + dottie@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/dottie/-/dottie-2.0.7.tgz#fd1b465a1211b35d5660e95b69bb6d5b5809b3c4" @@ -8250,6 +8396,11 @@ express@^4.17.1, express@^4.18.2: utils-merge "1.0.1" vary "~1.1.2" +exsolve@^1.0.8: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.1.1.tgz#c055418255459b6ecde4e59de0060a3e97bc7572" + integrity sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g== + ext@^1.1.2: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -9138,11 +9289,23 @@ get-symbol-description@^1.1.0: es-errors "^1.3.0" get-intrinsic "^1.2.6" +get-tsconfig@4.14.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz#985d85c52a9903864280ccc2448d413fbf1efed8" + integrity sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA== + dependencies: + resolve-pkg-maps "^1.0.0" + getopts@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== +giget@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/giget/-/giget-3.3.1.tgz#4a4e610cd112e5dc478c6035986fdc19c76dad73" + integrity sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg== + git-log-parser@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/git-log-parser/-/git-log-parser-1.2.0.tgz#2e6a4c1b13fc00028207ba795a7ac31667b9fd4a" @@ -10162,6 +10325,11 @@ is-docker@2.2.1, is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -10212,6 +10380,18 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== +is-in-ssh@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz#8eb73c1cabba77748d389588eeea132a63057622" + integrity sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw== + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-interactive@1.0.0, is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" @@ -10481,6 +10661,13 @@ is-wsl@2.2.0, is-wsl@^2.1.1, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f" + integrity sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw== + dependencies: + is-inside-container "^1.0.0" + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -10973,6 +11160,11 @@ jest@^29.3.1: import-local "^3.0.2" jest-cli "^29.7.0" +jiti@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64" + integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== + jju@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" @@ -11023,6 +11215,13 @@ js-yaml@4.1.1, js-yaml@^4.1.0, js-yaml@^4.3.1: dependencies: argparse "^2.0.1" +js-yaml@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524" + integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.15.1: version "3.15.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.1.tgz#24bc95028f361cdaaa84745b06a109c4486773c0" @@ -13642,6 +13841,11 @@ object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" +ohash@^2.0.11: + version "2.0.12" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.12.tgz#cb97c19888ff31e7b22214cd7924187fc97ef05c" + integrity sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw== + oidc-token-hash@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" @@ -13692,6 +13896,18 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== +open@11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/open/-/open-11.0.0.tgz#897e6132f994d3554cbcf72e0df98f176a7e5f62" + integrity sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw== + dependencies: + default-browser "^5.4.0" + define-lazy-prop "^3.0.0" + is-in-ssh "^1.0.0" + is-inside-container "^1.0.0" + powershell-utils "^0.1.0" + wsl-utils "^0.3.0" + open@7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69" @@ -14249,6 +14465,16 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz#e7078e38f231cb191855c3136a4423aef725d261" + integrity sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g== + pg-cloudflare@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" @@ -14483,6 +14709,15 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-types@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.3.1.tgz#fa27ed0940efcf40bba453b0e5cab41217b0d442" + integrity sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg== + dependencies: + confbox "^0.2.4" + exsolve "^1.0.8" + pathe "^2.0.3" + pluralize@8.0.0, pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" @@ -14528,6 +14763,11 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +powershell-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" + integrity sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A== + prebuild-install@^7.1.1, prebuild-install@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" @@ -14852,6 +15092,14 @@ raw-body@~2.5.3: iconv-lite "~0.4.24" unpipe "~1.0.0" +rc9@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rc9/-/rc9-3.0.1.tgz#3895e5834a2b5c2d8fb76d93e802fbcbc2579bc7" + integrity sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ== + dependencies: + defu "^6.1.6" + destr "^2.0.5" + rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -15015,6 +15263,11 @@ readable-stream@^4.2.0: process "^0.11.10" string_decoder "^1.3.0" +readdirp@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-5.1.1.tgz#520bca06f9d1ae1b96cc0800dbe84b983d19422c" + integrity sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -15186,6 +15439,11 @@ resolve-global@1.0.0, resolve-global@^1.0.0: dependencies: global-dirs "^0.1.1" +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve.exports@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" @@ -15294,6 +15552,11 @@ router@^2.2.0: parseurl "^1.3.3" path-to-regexp "^8.0.0" +run-applescript@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" + integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== + run-async@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -15526,6 +15789,11 @@ semver@7.7.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@7.8.4: + version "7.8.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.4.tgz#c73eceebae0616934be8dff28a7fd70757c8e696" + integrity sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA== + semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -17836,6 +18104,14 @@ ws@^8.20.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== +wsl-utils@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.3.1.tgz#9479836ddf03be267aad3abfc3cb1f6e0c9f1ed1" + integrity sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg== + dependencies: + is-wsl "^3.1.0" + powershell-utils "^0.1.0" + xml-naming@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/xml-naming/-/xml-naming-0.1.0.tgz#8ab7106c5b8d23caa2fabac1cadf17136379fbd8" From 90b37ef88ef5582fad60093a49e4b5a2d3985d0a Mon Sep 17 00:00:00 2001 From: Nicolas Bouliol Date: Wed, 19 Aug 2026 12:02:13 +0200 Subject: [PATCH 2/3] test(agent-bff): report a codegen or emission failure instead of burying it A failing step now throws with what the tool said: the emission carries the CLI stderr it silenced, and the codegen carries its own output plus the artifact it never wrote. The exit status alone was not enough - this codegen reports a missing input while exiting 0. Also bounds the codegen child, since spawnSync blocks the event loop and the beforeAll deadline could never fire, and awaits the server close so the port cannot leak into the next suite. Co-Authored-By: Claude Opus 5 (1M context) --- .../openapi/openapi-generated-client.test.ts | 66 ++++++++++++++----- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/agent-bff/test/openapi/openapi-generated-client.test.ts b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts index c18e64a65c..4839431efc 100644 --- a/packages/agent-bff/test/openapi/openapi-generated-client.test.ts +++ b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts @@ -6,7 +6,7 @@ import type { Server } from 'http'; import { bodyParser } from '@koa/bodyparser'; import { spawnSync } from 'child_process'; -import { readFileSync, rmSync } from 'fs'; +import { existsSync, readFileSync, rmSync } from 'fs'; import Koa from 'koa'; import path from 'path'; @@ -76,12 +76,15 @@ const RECORD_ID = '1'; const GENERATED_DIR = path.join(__dirname, '.generated'); const DOCUMENT_FILE = path.join(GENERATED_DIR, 'openapi.json'); const CLIENT_DIR = path.join(GENERATED_DIR, 'client'); +const SDK_FILE = path.join(CLIENT_DIR, 'sdk.gen.ts'); const CODEGEN_BIN = path.join( path.dirname(require.resolve('@hey-api/openapi-ts/package.json')), 'bin/run.js', ); +const CODEGEN_TIMEOUT_MS = 60_000; + const noopLogger: Logger = () => undefined; interface CallResult { @@ -188,7 +191,7 @@ function documentedOperators(document: { } describe('a client generated from the emitted OpenAPI document', () => { - let codegen: { status: number | null; output: string }; + let codegenOutput: string; let document: ReturnType; let sdk: Record; let server: Server; @@ -196,26 +199,47 @@ describe('a client generated from the emitted OpenAPI document', () => { beforeAll(async () => { rmSync(GENERATED_DIR, { recursive: true, force: true }); + // The command writes its own progress to stderr. Silenced so the run stays readable, but kept: + // it is the only account of why an emission failed. const stderr = jest.spyOn(process.stderr, 'write').mockReturnValue(true); - const emitted = await dispatchCli(['openapi', '--output', DOCUMENT_FILE], ENV, noopLogger); - stderr.mockRestore(); - - if (emitted.exitCode !== 0) throw new Error('The CLI could not emit the document'); + const emitted = await dispatchCli( + ['openapi', '--output', DOCUMENT_FILE], + ENV, + noopLogger, + ).finally(() => stderr.mockRestore()); + + if (emitted.exitCode !== 0) { + throw new Error( + `The CLI could not emit the document: ${stderr.mock.calls.flat().join('')}`.trim(), + ); + } + // Timed out rather than left to the `beforeAll` deadline: `spawnSync` blocks the event loop, so a + // codegen child that hangs would hang the whole Jest run until CI kills the job. `--no-log-file` + // because a failing run otherwise drops an `openapi-ts-error-*.log` in the working directory. const result = spawnSync( process.execPath, - [CODEGEN_BIN, '--input', DOCUMENT_FILE, '--output', CLIENT_DIR, '--silent'], - { encoding: 'utf8' }, + [CODEGEN_BIN, '--input', DOCUMENT_FILE, '--output', CLIENT_DIR, '--silent', '--no-log-file'], + { encoding: 'utf8', timeout: CODEGEN_TIMEOUT_MS }, ); - codegen = { - status: result.status, - output: `${result.stdout ?? ''}${result.stderr ?? ''}`, - }; + codegenOutput = `${result.stdout ?? ''}${result.stderr ?? ''}`; + + // Before requiring anything it generated: a failed codegen leaves no `sdk.gen.ts`, and the + // MODULE_NOT_FOUND that follows would bury the tool's own account of what it choked on. The file + // is checked as well as the status, because this codegen reports some failures — a missing input + // among them — on its output while still exiting 0. + if (result.status !== 0 || !existsSync(SDK_FILE)) { + throw new Error( + `The codegen could not consume the document (status ${result.status}${ + result.error ? `, ${result.error.message}` : '' + }): ${codegenOutput || `nothing was written to ${SDK_FILE}`}`.trim(), + ); + } document = JSON.parse(readFileSync(DOCUMENT_FILE, 'utf8')); // eslint-disable-next-line global-require, import/no-dynamic-require - sdk = require(path.join(CLIENT_DIR, 'sdk.gen.ts')); + sdk = require(SDK_FILE); // eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires const { client } = require(path.join(CLIENT_DIR, 'client.gen.ts')) as { client: GeneratedClient; @@ -229,10 +253,16 @@ describe('a client generated from the emitted OpenAPI document', () => { auth: () => API_KEY, headers: { [TIMEZONE_HEADER]: TIMEZONE }, }); - }, 60_000); + }, CODEGEN_TIMEOUT_MS * 2); + + afterAll(async () => { + // Awaited: a fire-and-forget close can leave the port open into the next suite. + if (server?.listening) { + await new Promise((resolve, reject) => { + server.close(error => (error ? reject(error) : resolve())); + }); + } - afterAll(() => { - server?.close(); rmSync(GENERATED_DIR, { recursive: true, force: true }); }); @@ -241,8 +271,8 @@ describe('a client generated from the emitted OpenAPI document', () => { }); describe('when a standard codegen reads the document', () => { - it('should generate without failing, since a consumer runs this before anything else', () => { - expect(codegen).toEqual({ status: 0, output: '' }); + it('should generate without a warning, since a warning is the document confusing the tool', () => { + expect(codegenOutput).toBe(''); }); it('should expose one function per documented operation, named after its operationId', () => { From 1bf8e7786108d493c15a1b99e1716f4f28c5cfb6 Mon Sep 17 00:00:00 2001 From: Nicolas Bouliol Date: Fri, 21 Aug 2026 15:06:02 +0200 Subject: [PATCH 3/3] test(agent-bff): make the encoding and enum checks able to fail Both were vacuous. The encoding one passed on a raw document because undici percent-encodes client-side, so the fixture now carries an action named "Facture 50/50": raw, its path splits into segments no route matches. The enum one compared the document against itself, so it now asserts the literal before filtering the generated types. Also captures the CLI stderr before restoring the spy, since mockRestore clears mock.calls and the failure message was always empty, and clears NODE_OPTIONS for the codegen child so an inherited Node warning cannot fail an assertion about the document. The ignored path for the generated client moves to the package, next to how datasource-sql and agent-testing ignore their own test artifacts. --- .gitignore | 3 -- packages/agent-bff/.gitignore | 2 + .../openapi/openapi-generated-client.test.ts | 47 +++++++++++++++---- 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 packages/agent-bff/.gitignore diff --git a/.gitignore b/.gitignore index 97585b0f5f..02554be26a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,9 +18,6 @@ lerna-debug.log # forest-bff openapi --output default destination openapi.json -# document and client written by the OpenAPI codegen verification test -packages/agent-bff/test/openapi/.generated - # yarn yarn-error.log .vscode/settings.json diff --git a/packages/agent-bff/.gitignore b/packages/agent-bff/.gitignore new file mode 100644 index 0000000000..e8613505a6 --- /dev/null +++ b/packages/agent-bff/.gitignore @@ -0,0 +1,2 @@ +# document and client written by the OpenAPI codegen verification test +test/openapi/.generated diff --git a/packages/agent-bff/test/openapi/openapi-generated-client.test.ts b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts index 4839431efc..09c7f80911 100644 --- a/packages/agent-bff/test/openapi/openapi-generated-client.test.ts +++ b/packages/agent-bff/test/openapi/openapi-generated-client.test.ts @@ -20,6 +20,8 @@ import { action, collection, column, relation } from '../read-model/fixtures'; const MARK_AS_PAID = 'Mark as paid'; const GENERATE_INVOICE = 'Générer la facture'; +// A name carrying the path separator: raw in the document it would not resolve to any route. +const SPLIT_INVOICE = 'Facture 50/50'; // One fixture for both sides: the document is generated from this schema and these capabilities, and // the chain the generated client calls runs on the very same ones. Two sources would let the test go @@ -31,6 +33,7 @@ const SCHEMA = [ [ action(MARK_AS_PAID, '/forest/users/actions/mark-as-paid'), action(GENERATE_INVOICE, '/forest/users/actions/generer-la-facture'), + action(SPLIT_INVOICE, '/forest/users/actions/facture-50-50'), ], ), collection('orders', [column('id')]), @@ -110,6 +113,8 @@ const OPERATIONS = [ 'executeActionUsersMarkAsPaid', 'getActionFormUsersGNRerLaFacture', 'executeActionUsersGNRerLaFacture', + 'getActionFormUsersFacture5050', + 'executeActionUsersFacture5050', ] as const; const dataClient: AgentDataClient = { @@ -201,17 +206,21 @@ describe('a client generated from the emitted OpenAPI document', () => { // The command writes its own progress to stderr. Silenced so the run stays readable, but kept: // it is the only account of why an emission failed. + // Read out before restoring, not after: `mockRestore` clears `mock.calls`, so a message built + // from the spy afterwards would always be empty — which is the whole account of a failure here. const stderr = jest.spyOn(process.stderr, 'write').mockReturnValue(true); + let written = ''; const emitted = await dispatchCli( ['openapi', '--output', DOCUMENT_FILE], ENV, noopLogger, - ).finally(() => stderr.mockRestore()); + ).finally(() => { + written = stderr.mock.calls.flat().join(''); + stderr.mockRestore(); + }); if (emitted.exitCode !== 0) { - throw new Error( - `The CLI could not emit the document: ${stderr.mock.calls.flat().join('')}`.trim(), - ); + throw new Error(`The CLI could not emit the document: ${written}`.trim()); } // Timed out rather than left to the `beforeAll` deadline: `spawnSync` blocks the event loop, so a @@ -220,7 +229,13 @@ describe('a client generated from the emitted OpenAPI document', () => { const result = spawnSync( process.execPath, [CODEGEN_BIN, '--input', DOCUMENT_FILE, '--output', CLIENT_DIR, '--silent', '--no-log-file'], - { encoding: 'utf8', timeout: CODEGEN_TIMEOUT_MS }, + { + encoding: 'utf8', + timeout: CODEGEN_TIMEOUT_MS, + // Cleared so a Node warning inherited from the parent cannot land in the output this file + // asserts on: what is measured here is the document, not the runner's flags. + env: { ...process.env, NODE_OPTIONS: '' }, + }, ); codegenOutput = `${result.stdout ?? ''}${result.stderr ?? ''}`; @@ -282,9 +297,12 @@ describe('a client generated from the emitted OpenAPI document', () => { it('should carry the documented operator set into the generated types', () => { const types = readFileSync(path.join(CLIENT_DIR, 'types.gen.ts'), 'utf8'); - expect( - documentedOperators(document).filter(operator => !types.includes(`'${operator}'`)), - ).toEqual([]); + const operators = documentedOperators(document); + + // Asserted against the literal first: both sides of the filter below derive from the document, + // so an empty enum would satisfy it vacuously. + expect(operators).toEqual([DOCUMENTED_OPERATOR]); + expect(operators.filter(operator => !types.includes(`'${operator}'`))).toEqual([]); }); }); @@ -381,7 +399,7 @@ describe('a client generated from the emitted OpenAPI document', () => { }); }); - it('should reach an action whose name needs URL-encoding beyond spaces', async () => { + it('should reach an action whose name carries an accent', async () => { const form = await sdk.getActionFormUsersGNRerLaFacture({ body: { recordIds: [RECORD_ID] } }); const executed = await sdk.executeActionUsersGNRerLaFacture({ body: { recordIds: [RECORD_ID] }, @@ -389,6 +407,17 @@ describe('a client generated from the emitted OpenAPI document', () => { expect([form.response.status, executed.response.status]).toEqual([200, 200]); }); + + // The separator is what makes this more than a formality: a raw name would split the path into + // segments the route cannot match, so only a percent-encoded document resolves here. + it('should reach an action whose name carries the path separator', async () => { + const form = await sdk.getActionFormUsersFacture5050({ body: { recordIds: [RECORD_ID] } }); + const executed = await sdk.executeActionUsersFacture5050({ + body: { recordIds: [RECORD_ID] }, + }); + + expect([form.response.status, executed.response.status]).toEqual([200, 200]); + }); }); describe("when the generated client filters on a documented field's operator", () => {