From 680a528f27fb88e5836fd13a03060ea5fdb9f406 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 10:52:14 +0200 Subject: [PATCH 01/12] Test the runtime's ESM resolve hook --- .../adaptor-with-subpath/index.js | 3 ++ .../adaptor-with-subpath/package.json | 7 ++++ .../node_modules/cjs-subpaths/package.json | 5 +++ .../node_modules/cjs-subpaths/set.js | 1 + .../runtime/test/__esm-hook__/run-job.mjs | 38 ++++++++++++++++++ .../runtime/test/modules/esm-hook.test.ts | 40 +++++++++++++++++++ 6 files changed, 94 insertions(+) create mode 100644 packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js create mode 100644 packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json create mode 100644 packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json create mode 100644 packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js create mode 100644 packages/runtime/test/__esm-hook__/run-job.mjs create mode 100644 packages/runtime/test/modules/esm-hook.test.ts diff --git a/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js new file mode 100644 index 000000000..3426a81dc --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/index.js @@ -0,0 +1,3 @@ +import set from 'cjs-subpaths/set'; + +export const value = set; diff --git a/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json new file mode 100644 index 000000000..5f05eca12 --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/adaptor-with-subpath/package.json @@ -0,0 +1,7 @@ +{ + "name": "adaptor-with-subpath", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "private": true +} diff --git a/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json new file mode 100644 index 000000000..4b01f1e0c --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/package.json @@ -0,0 +1,5 @@ +{ + "name": "cjs-subpaths", + "version": "1.0.0", + "private": true +} diff --git a/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js new file mode 100644 index 000000000..571faddbd --- /dev/null +++ b/packages/runtime/test/__esm-hook__/node_modules/cjs-subpaths/set.js @@ -0,0 +1 @@ +module.exports = 'set'; diff --git a/packages/runtime/test/__esm-hook__/run-job.mjs b/packages/runtime/test/__esm-hook__/run-job.mjs new file mode 100644 index 000000000..c57a21fb8 --- /dev/null +++ b/packages/runtime/test/__esm-hook__/run-job.mjs @@ -0,0 +1,38 @@ +// Runs in its own process so that every test starts with no hooks registered +import path from 'node:path'; +import run, { registerEsmHook } from '../../dist/index.js'; + +const [action, arg] = process.argv.slice(2); +const here = path.dirname(new URL(import.meta.url).pathname); + +const actions = { + async job() { + const state = await run( + `import { value } from 'adaptor-with-subpath'; + export default [() => ({ value })];`, + {}, + { + disableEsmHook: arg === 'no-hook', + linker: { + modules: { + 'adaptor-with-subpath': { + path: path.join(here, 'node_modules/adaptor-with-subpath'), + }, + }, + }, + } + ); + return state.value; + }, + async import() { + registerEsmHook(); + const m = await import(arg); + return m.default; + }, +}; + +try { + console.log(JSON.stringify({ result: await actions[action]() })); +} catch (e) { + console.log(JSON.stringify({ error: e.code ?? e.name })); +} diff --git a/packages/runtime/test/modules/esm-hook.test.ts b/packages/runtime/test/modules/esm-hook.test.ts new file mode 100644 index 000000000..442443ae2 --- /dev/null +++ b/packages/runtime/test/modules/esm-hook.test.ts @@ -0,0 +1,40 @@ +import test from 'ava'; +import path from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const script = path.resolve('test/__esm-hook__/run-job.mjs'); + +const exec = async (...args: string[]) => { + const { stdout } = await promisify(execFile)( + process.execPath, + ['--no-warnings', '--experimental-vm-modules', script, ...args], + { cwd: path.dirname(script) } + ); + return JSON.parse(stdout); +}; + +test('a job can use an adaptor that imports an extensionless CJS subpath', async (t) => { + const { result } = await exec('job'); + t.is(result, 'set'); +}); + +test('without the hook, the same job fails to import the adaptor', async (t) => { + const { error } = await exec('job', 'no-hook'); + t.is(error, 'ImportError'); +}); + +test('the hook resolves an extensionless CJS subpath', async (t) => { + const { result } = await exec('import', 'cjs-subpaths/set'); + t.is(result, 'set'); +}); + +test('the hook leaves a specifier with an extension alone', async (t) => { + const { result } = await exec('import', 'cjs-subpaths/set.js'); + t.is(result, 'set'); +}); + +test('the hook still fails on a subpath that does not exist', async (t) => { + const { error } = await exec('import', 'cjs-subpaths/missing'); + t.is(error, 'ERR_MODULE_NOT_FOUND'); +}); From e3277cae1e29e765980f0085bcaebf0e09743083 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 10:52:32 +0200 Subject: [PATCH 02/12] Build the worker image on Node 26 bookworm-slim --- .changeset/worker-node-26-bookworm.md | 5 +++++ .tool-versions | 2 +- Dockerfile | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/worker-node-26-bookworm.md diff --git a/.changeset/worker-node-26-bookworm.md b/.changeset/worker-node-26-bookworm.md new file mode 100644 index 000000000..c6917e7d0 --- /dev/null +++ b/.changeset/worker-node-26-bookworm.md @@ -0,0 +1,5 @@ +--- +'@openfn/ws-worker': minor +--- + +Run the worker image on Node 26 and Debian bookworm diff --git a/.tool-versions b/.tool-versions index 1cdceb670..6f1d47511 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 26.5.1 +nodejs 26.12.0 diff --git a/Dockerfile b/Dockerfile index 3bb4b610f..edc51835c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM node:24-alpine AS base +ARG NODE_VERSION=26.12.0 + +FROM node:${NODE_VERSION}-bookworm-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" COPY . /app @@ -18,7 +20,7 @@ RUN npm install -g "$(node -p "require('./package.json').packageManager.split('+ # TODO: remove simple build once prod optimized build is working --------------- FROM base AS ws-worker -RUN apk add --no-cache git +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/* RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN pnpm build WORKDIR /app/packages/ws-worker From 40f0c2b4c400cc272b6ecb6f573f667192f1d2db Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 10:53:07 +0200 Subject: [PATCH 03/12] Move devcontainer and integration-test images to Node 26 on bookworm The devcontainer reads Node's version from .tool-versions through nvm and installs pnpm at the packageManager version. The integration-test images use node:${NODE_VERSION}-bookworm-slim. --- .devcontainer/devcontainer.json | 8 ++++---- .devcontainer/install-node.sh | 10 ++++++++++ integration-tests/cli/Dockerfile | 3 ++- integration-tests/worker/Dockerfile | 3 ++- 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100755 .devcontainer/install-node.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e35a6be6c..d780253fe 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Default", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "image": "mcr.microsoft.com/devcontainers/base:bookworm", "features": { "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { @@ -13,7 +13,7 @@ }, "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, - "version": "18" + "version": "none" } }, @@ -25,8 +25,8 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install -g pnpm" + // Node comes from .tool-versions, so the node feature only installs nvm. + "onCreateCommand": ".devcontainer/install-node.sh" // Configure tool-specific properties. // "customizations": {}, diff --git a/.devcontainer/install-node.sh b/.devcontainer/install-node.sh new file mode 100755 index 000000000..cdea067b4 --- /dev/null +++ b/.devcontainer/install-node.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +node_version=$(awk '$1 == "nodejs" { print $2 }' .tool-versions) + +. "$NVM_DIR/nvm.sh" +nvm install "$node_version" +nvm alias default "$node_version" + +npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")" diff --git a/integration-tests/cli/Dockerfile b/integration-tests/cli/Dockerfile index 4bcd59456..2baee63be 100644 --- a/integration-tests/cli/Dockerfile +++ b/integration-tests/cli/Dockerfile @@ -1,6 +1,7 @@ # This dockerfile allows us to run tests against the build in isolation # (I'm not really sure we need it but it's been a useful dev tool) -FROM node:18.12.0 +ARG NODE_VERSION=26.12.0 +FROM node:${NODE_VERSION}-bookworm-slim ENV NODE_ENV=production diff --git a/integration-tests/worker/Dockerfile b/integration-tests/worker/Dockerfile index c1b0072bc..fcd6d8e00 100644 --- a/integration-tests/worker/Dockerfile +++ b/integration-tests/worker/Dockerfile @@ -1,6 +1,7 @@ # This dockerfile allows us to run tests against the build in isolation # (I'm not really sure we need it but it's been a useful dev tool) -FROM node:18.12.0 +ARG NODE_VERSION=26.12.0 +FROM node:${NODE_VERSION}-bookworm-slim ENV NODE_ENV=production From bdc22c316e1d2e3eea2061fc2c69a27852b706af Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 10:53:35 +0200 Subject: [PATCH 04/12] Register the runtime's ESM hook with module.registerHooks() Falls back to module.register() on Node versions without registerHooks, and registers once per thread so repeated runs don't chain the hook. --- .changeset/quiet-hooks-register.md | 5 +++++ packages/runtime/package.json | 3 +-- .../runtime/src/modules/esm-resolve-hook.js | 18 ++++++++++++++- .../runtime/src/modules/register-esm-hook.ts | 22 +++++++++++++++++-- .../test/__esm-hook__/count-resolves.mjs | 8 +++++++ .../runtime/test/__esm-hook__/run-job.mjs | 13 +++++++++++ .../runtime/test/modules/esm-hook.test.ts | 16 ++++++++++++-- 7 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 .changeset/quiet-hooks-register.md create mode 100644 packages/runtime/test/__esm-hook__/count-resolves.mjs diff --git a/.changeset/quiet-hooks-register.md b/.changeset/quiet-hooks-register.md new file mode 100644 index 000000000..d946ef223 --- /dev/null +++ b/.changeset/quiet-hooks-register.md @@ -0,0 +1,5 @@ +--- +'@openfn/runtime': patch +--- + +Use `module.registerHooks()` for the ESM resolve hook where Node supports it diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2cc920459..13b2aa5f3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -21,7 +21,7 @@ "test:watch": "pnpm ava -w", "test:types": "pnpm tsc --project tsconfig.test.json", "test:memory": "pnpm ava --config memtest.ava.config.cjs", - "build": "tsup --config ../../tsup.config.js src/index.ts src/modules/esm-resolve-hook.js", + "build": "tsup --config ../../tsup.config.js --no-splitting src/index.ts src/modules/esm-resolve-hook.js", "build:watch": "pnpm build --watch", "pack": "pnpm pack --pack-destination ../../dist" }, @@ -48,7 +48,6 @@ "dist/index.d.ts", "dist/modules/esm-resolve-hook.js", "dist/modules/esm-resolve-hook.d.ts", - "dist/esm-resolve-hook.js", "README.md" ], "dependencies": { diff --git a/packages/runtime/src/modules/esm-resolve-hook.js b/packages/runtime/src/modules/esm-resolve-hook.js index cc6b71f17..41ab6b812 100644 --- a/packages/runtime/src/modules/esm-resolve-hook.js +++ b/packages/runtime/src/modules/esm-resolve-hook.js @@ -1,8 +1,24 @@ +const shouldRetry = (err, specifier) => + err.code === 'ERR_MODULE_NOT_FOUND' && !/\.[a-z]+$/.test(specifier); + +// For module.registerHooks() +export function resolveSync(specifier, context, nextResolve) { + try { + return nextResolve(specifier, context); + } catch (err) { + if (shouldRetry(err, specifier)) { + return nextResolve(specifier + '.js', context); + } + throw err; + } +} + +// For module.register(), which loads this file on a separate thread export async function resolve(specifier, context, nextResolve) { try { return await nextResolve(specifier, context); } catch (err) { - if (err.code === 'ERR_MODULE_NOT_FOUND' && !/\.[a-z]+$/.test(specifier)) { + if (shouldRetry(err, specifier)) { return nextResolve(specifier + '.js', context); } throw err; diff --git a/packages/runtime/src/modules/register-esm-hook.ts b/packages/runtime/src/modules/register-esm-hook.ts index eaf42e12a..289a44502 100644 --- a/packages/runtime/src/modules/register-esm-hook.ts +++ b/packages/runtime/src/modules/register-esm-hook.ts @@ -1,10 +1,28 @@ -import { register } from 'node:module'; +import module from 'node:module'; import path from 'node:path'; import url from 'node:url'; +import { resolveSync } from './esm-resolve-hook.js'; + +let registered = false; // Backcompat loader hook for ESM modules that import CJS subpaths without a .js extension // e.g. `import set from 'lodash/set'` instead of `import set from 'lodash/set.js'` +// Hooks chain on every registration, so this registers once per thread export const registerEsmHook = () => { + if (registered) { + return; + } + registered = true; + + // registerHooks() needs Node 22.15 or 23.5 + const { registerHooks } = module as typeof module & { + registerHooks?: (hooks: { resolve: typeof resolveSync }) => void; + }; + if (registerHooks) { + registerHooks({ resolve: resolveSync }); + return; + } + try { // Resolve relative to the package entry point so this works both from source // (where import.meta.url is src/modules/) and from the bundle (where it's dist/) @@ -15,7 +33,7 @@ export const registerEsmHook = () => { path.dirname(packageEntry), 'modules/esm-resolve-hook.js' ); - register(`file://${hookPath}`); + module.register(`file://${hookPath}`); } catch (e) { console.error(e); } diff --git a/packages/runtime/test/__esm-hook__/count-resolves.mjs b/packages/runtime/test/__esm-hook__/count-resolves.mjs new file mode 100644 index 000000000..1e42e00ba --- /dev/null +++ b/packages/runtime/test/__esm-hook__/count-resolves.mjs @@ -0,0 +1,8 @@ +import { writeSync } from 'node:fs'; + +// Writes each specifier it resolves to stderr. Registered beneath the ESM hook, +// it sees every attempt the hook makes. +export function resolve(specifier, context, nextResolve) { + writeSync(2, `${specifier}\n`); + return nextResolve(specifier, context); +} diff --git a/packages/runtime/test/__esm-hook__/run-job.mjs b/packages/runtime/test/__esm-hook__/run-job.mjs index c57a21fb8..5c0bb44e7 100644 --- a/packages/runtime/test/__esm-hook__/run-job.mjs +++ b/packages/runtime/test/__esm-hook__/run-job.mjs @@ -1,4 +1,5 @@ // Runs in its own process so that every test starts with no hooks registered +import module from 'node:module'; import path from 'node:path'; import run, { registerEsmHook } from '../../dist/index.js'; @@ -29,6 +30,18 @@ const actions = { const m = await import(arg); return m.default; }, + async 'import-after-registering-three-times'() { + const counter = new URL('./count-resolves.mjs', import.meta.url); + if (module.registerHooks) { + module.registerHooks(await import(counter)); + } else { + module.register(counter); + } + registerEsmHook(); + registerEsmHook(); + registerEsmHook(); + await import(arg); + }, }; try { diff --git a/packages/runtime/test/modules/esm-hook.test.ts b/packages/runtime/test/modules/esm-hook.test.ts index 442443ae2..48d73783b 100644 --- a/packages/runtime/test/modules/esm-hook.test.ts +++ b/packages/runtime/test/modules/esm-hook.test.ts @@ -6,12 +6,12 @@ import { promisify } from 'node:util'; const script = path.resolve('test/__esm-hook__/run-job.mjs'); const exec = async (...args: string[]) => { - const { stdout } = await promisify(execFile)( + const { stdout, stderr } = await promisify(execFile)( process.execPath, ['--no-warnings', '--experimental-vm-modules', script, ...args], { cwd: path.dirname(script) } ); - return JSON.parse(stdout); + return { ...JSON.parse(stdout), stderr }; }; test('a job can use an adaptor that imports an extensionless CJS subpath', async (t) => { @@ -38,3 +38,15 @@ test('the hook still fails on a subpath that does not exist', async (t) => { const { error } = await exec('import', 'cjs-subpaths/missing'); t.is(error, 'ERR_MODULE_NOT_FOUND'); }); + +test('registering the hook again does not add it twice', async (t) => { + const { error, stderr } = await exec( + 'import-after-registering-three-times', + 'cjs-subpaths/missing' + ); + t.is(error, 'ERR_MODULE_NOT_FOUND'); + const attempts = stderr + .split('\n') + .filter((line: string) => line.startsWith('cjs-subpaths/missing')); + t.deepEqual(attempts, ['cjs-subpaths/missing', 'cjs-subpaths/missing.js']); +}); From 50a8235a0c7206fb84662b95821da54256e15b59 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 11:00:32 +0200 Subject: [PATCH 05/12] Run CircleCI on Node 26.12.0 with a Node version drift check Every job except legacy_test checks node -v, the Dockerfile NODE_VERSION ARGs and the cimg/node tags against .tool-versions, and installs pnpm at the packageManager version now corepack is gone. The integration matrix drops Node 22. A new worker_image job builds the worker image, checks its Node version and loads motherduck as user node. --- .circleci/config.yml | 77 +++++++++++++++++++++++++++++++---- scripts/check-node-version.sh | 77 +++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 9 deletions(-) create mode 100755 scripts/check-node-version.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c836c967..dd5651a7d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,22 @@ commands: name: install CLI globally command: sudo npm install -g file:dist/openfn-cli.tgz + check_node_version: + parameters: + node_version: + type: string + default: '' + steps: + - run: + name: Check Node versions match .tool-versions + command: scripts/check-node-version.sh << parameters.node_version >> + + install_pnpm: + steps: + - run: + name: Install pnpm + command: sudo npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")" + # Define a job to be invoked later in a workflow. # See: https://circleci.com/docs/2.0/configuration-reference/#jobs jobs: @@ -16,22 +32,24 @@ jobs: # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium # Add steps to the job # See: https://circleci.com/docs/2.0/configuration-reference/#steps steps: - checkout + - check_node_version + - install_pnpm - restore_cache: # See the configuration reference documentation for more details on using restore_cache and save_cache steps # https://circleci.com/docs/2.0/configuration-reference/?section=reference#save_cache keys: - - deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}} + - deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}}-{{checksum ".tool-versions"}} - run: name: Install command: pnpm install --frozen-lockfile - save_cache: - key: deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}} + key: deps-v1-{{ .Branch }}-{{checksum "pnpm-lock.yaml"}}-{{checksum ".tool-versions"}} paths: - node_modules - persist_to_workspace: @@ -41,11 +59,13 @@ jobs: build: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Build packages command: pnpm build @@ -56,45 +76,53 @@ jobs: unit_test: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium parallelism: 1 steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Run Tests command: pnpm test format: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Check formatting command: pnpm test:format type_check: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Type check command: pnpm test:types build_openfnx: docker: - - image: cimg/node:24.18 + - image: cimg/node:26.12.0 resource_class: medium steps: - attach_workspace: at: ~/project + - check_node_version + - install_pnpm - run: name: Build local tarballs command: pnpm pack:local dist --no-version @@ -114,6 +142,9 @@ jobs: steps: - attach_workspace: at: ~/project + - check_node_version: + node_version: << parameters.node_version >> + - install_pnpm - install_cli_global - run: name: Check install @@ -122,6 +153,29 @@ jobs: name: Run integration test suite command: pnpm test:integration + worker_image: + docker: + - image: cimg/node:26.12.0 + resource_class: medium + steps: + - checkout + - check_node_version + - setup_remote_docker + - run: + name: Build worker image + command: docker build -t ws-worker-ci . + - run: + name: Check Node in the image matches .tool-versions + command: docker run --rm ws-worker-ci /app/scripts/check-node-version.sh + - run: + name: Install and load motherduck as user node + command: | + docker run --rm --user node ws-worker-ci sh -c ' + mkdir -p /tmp/repo && cd /tmp/repo && + npm install --no-audit --no-fund --no-package-lock --ignore-scripts @openfn/language-motherduck@latest && + node -e "require(\"@openfn/language-motherduck\")" + ' + # This ensures that we can run a v1 CLI deploy in node 18, # which is important for GH sync legacy_test: @@ -165,9 +219,14 @@ workflows: - integration_test: matrix: parameters: - node_version: ['22.20', '24.18.1', '26.5.1'] + node_version: ['24.22.0', '26.12.0'] requires: - build_openfnx - legacy_test: requires: - build_openfnx + - worker_image: + filters: + branches: + ignore: + - main diff --git a/scripts/check-node-version.sh b/scripts/check-node-version.sh new file mode 100755 index 000000000..03bf2c8a3 --- /dev/null +++ b/scripts/check-node-version.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Fails unless the running node, every Dockerfile's `ARG NODE_VERSION=` and the +# cimg/node tags in .circleci/config.yml all match .tool-versions. +# +# Usage: scripts/check-node-version.sh [expected-node-version] +# The argument overrides the version `node -v` must report, for CI jobs that +# deliberately run on another Node (the integration test matrix). + +set -euo pipefail + +cd "$(dirname "$0")/.." + +# legacy_test deliberately runs the v1 CLI deploy on Node 18 +EXEMPT_JOBS="legacy_test" + +want=$(awk '$1 == "nodejs" { print $2 }' .tool-versions) +if [ -z "$want" ]; then + echo "No nodejs entry in .tool-versions" >&2 + exit 1 +fi +major=${want%%.*} +errors=() + +expected_node=${1:-$want} +actual_node=$(node -v) +actual_node=${actual_node#v} +if [ "$actual_node" != "$expected_node" ]; then + errors+=("node -v is $actual_node, expected $expected_node") +fi + +while IFS= read -r dockerfile; do + while IFS= read -r arg; do + if [ "$arg" != "$want" ]; then + errors+=("$dockerfile: ARG NODE_VERSION=$arg") + fi + done < <(sed -n 's/^ARG NODE_VERSION=\([^[:space:]]*\).*/\1/p' "$dockerfile") +done < <(find . -name node_modules -prune -o -name .git -prune -o -type f -name 'Dockerfile*' -print | sort) + +config=.circleci/config.yml + +while read -r job tag; do + case " $EXEMPT_JOBS " in *" $job "*) continue ;; esac + if [ "$tag" != "$want" ]; then + errors+=("$config: job $job uses cimg/node:$tag") + fi +done < <(awk ' + /^[^ #]/ { section = $1 } + section == "jobs:" && /^ [A-Za-z0-9_-]+:/ { job = $1; sub(/:$/, "", job) } + /image: *cimg\/node:/ { + tag = $0 + sub(/.*cimg\/node:/, "", tag) + sub(/[[:space:]].*/, "", tag) + gsub(/["\047]/, "", tag) + if (tag !~ /^<&2 + printf ' %s\n' "${errors[@]}" >&2 + exit 1 +fi + +echo "Node pins match .tool-versions ($want), node -v is $actual_node" From 1089d68e07eb77b521c82819c44f121d0b2ccfa5 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 13:17:23 +0200 Subject: [PATCH 06/12] Delete the CLI integration-test image It hasn't built since the CLI integration tests took workspace dependencies, and nothing in CI uses it. The tests run with test:dev locally and through the globally installed CLI in CircleCI. --- integration-tests/cli/.dockerignore | 2 -- integration-tests/cli/Dockerfile | 16 ---------------- integration-tests/cli/README.md | 11 +++-------- integration-tests/cli/package.json | 3 --- integration-tests/legacy-deploy/package.json | 1 - 5 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 integration-tests/cli/.dockerignore delete mode 100644 integration-tests/cli/Dockerfile diff --git a/integration-tests/cli/.dockerignore b/integration-tests/cli/.dockerignore deleted file mode 100644 index c8c6ad9ba..000000000 --- a/integration-tests/cli/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -repo \ No newline at end of file diff --git a/integration-tests/cli/Dockerfile b/integration-tests/cli/Dockerfile deleted file mode 100644 index 2baee63be..000000000 --- a/integration-tests/cli/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# This dockerfile allows us to run tests against the build in isolation -# (I'm not really sure we need it but it's been a useful dev tool) -ARG NODE_VERSION=26.12.0 -FROM node:${NODE_VERSION}-bookworm-slim - -ENV NODE_ENV=production - -WORKDIR /app - -COPY . . - -RUN npm install -g ./dist/openfn-cli.tgz - -RUN npm install --production - -CMD ["npm", "test"] \ No newline at end of file diff --git a/integration-tests/cli/README.md b/integration-tests/cli/README.md index 28ec2ec25..fce4a2ec0 100644 --- a/integration-tests/cli/README.md +++ b/integration-tests/cli/README.md @@ -2,7 +2,7 @@ This package contains a set of integration tests against `openfn` CLI. -The test suite is designed to be run in CircleCI, but can run locally or in a local docker image. +The test suite is designed to be run in CircleCI, but can also run locally. ## Running Tests @@ -10,11 +10,6 @@ During development, you can run the unit tests against a local build (ie, `packa `pnpm test:dev` -You can run the test in isolation with a docker build. This can be useful while developing for CI, or if a test passes locally but fails in CI. - -`pnpm run build` -`pnpm run start` - ## Architecture The basic idea here is to test the built (but unreleased) CLI bundle in the global space. @@ -29,6 +24,6 @@ How it works: ## Creating tarballs -From this folder, run `build:pack` to create a bunch of tarballs from your built packages. Tarballs will be built to `dist`. +From the repo root, run `pnpm pack:local integration-tests/cli/dist --no-version` to create a bunch of tarballs from your built packages. Tarballs will be built to `dist`. -You can install the tarballs locally with `npm install -g dist openfn-cli.tgz.` +From this folder, install the CLI from the tarballs with `npm install -g ./dist/openfn-cli.tgz`. diff --git a/integration-tests/cli/package.json b/integration-tests/cli/package.json index 74bf51651..87435e058 100644 --- a/integration-tests/cli/package.json +++ b/integration-tests/cli/package.json @@ -8,9 +8,6 @@ "type": "module", "scripts": { "clean": "rimraf dist repo", - "build:pack": "pnpm clean && cd ../.. && pnpm pack:local integration-tests/cli/dist --no-version", - "build": "pnpm build:pack && docker build --tag cli-integration-tests .", - "start": "docker run cli-integration-tests", "test": "node prelude.js && npx ava -s --timeout 2m && pnpm clean", "test:dev": "node prelude.js && pnpm ava -s && pnpm clean" }, diff --git a/integration-tests/legacy-deploy/package.json b/integration-tests/legacy-deploy/package.json index 7eb35b66c..b9a6f59a8 100644 --- a/integration-tests/legacy-deploy/package.json +++ b/integration-tests/legacy-deploy/package.json @@ -8,7 +8,6 @@ "type": "module", "scripts": { "clean": "rimraf dist repo", - "start": "docker run cli-integration-tests", "test": "npx ava -s" }, "dependencies": { From f1e68971a83305c8c9baef431473ed0930781b86 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 14:54:55 +0200 Subject: [PATCH 07/12] Upgrade the worker to Sentry 11 to stop DEP0205 @sentry/node-core 10.x called module.register() from Sentry.init, which Node 26 deprecates. 11.x no longer does, so the upgrade alone fixes it. 11.x turns on the Dedupe integration by default, which drops an event identical to the one before it. The test helper turns it off, since otherwise one test's report is swallowed by an earlier test's. --- .changeset/worker-sentry-11.md | 5 + packages/ws-worker/package.json | 2 +- packages/ws-worker/test/util.ts | 3 + pnpm-lock.yaml | 1255 ++++++++++++------------------- 4 files changed, 498 insertions(+), 767 deletions(-) create mode 100644 .changeset/worker-sentry-11.md diff --git a/.changeset/worker-sentry-11.md b/.changeset/worker-sentry-11.md new file mode 100644 index 000000000..a27dfb75c --- /dev/null +++ b/.changeset/worker-sentry-11.md @@ -0,0 +1,5 @@ +--- +'@openfn/ws-worker': patch +--- + +Upgrade Sentry to v11 to stop a Node deprecation warning at startup diff --git a/packages/ws-worker/package.json b/packages/ws-worker/package.json index 9a07513dc..6d5fead96 100644 --- a/packages/ws-worker/package.json +++ b/packages/ws-worker/package.json @@ -29,7 +29,7 @@ "@openfn/lexicon": "workspace:^", "@openfn/logger": "workspace:*", "@openfn/runtime": "workspace:*", - "@sentry/node": "^10.44.0", + "@sentry/node": "^11.8.0", "fast-safe-stringify": "^2.1.1", "figures": "^5.0.0", "human-id": "^4.1.3", diff --git a/packages/ws-worker/test/util.ts b/packages/ws-worker/test/util.ts index 5fcf1e63a..24916d931 100644 --- a/packages/ws-worker/test/util.ts +++ b/packages/ws-worker/test/util.ts @@ -13,6 +13,9 @@ export const initSentry = () => { Sentry.init({ dsn: 'https://296274784378f87245c369278a62b29a@o55451.ingest.us.sentry.io/4508936084848640', transport: sentryTransport, + // Dedupe drops an event identical to the previous one, so a test that + // repeats an earlier test's error would never see its report + integrations: (defaults) => defaults.filter((i) => i.name !== 'Dedupe'), }); return testkit; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a741c902..e2433c190 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,10 +174,10 @@ importers: version: 2.30.0(@types/node@25.6.0) '@slack/web-api': specifier: ^7.16.0 - version: 7.16.0 + version: 7.16.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) '@swc-node/register': specifier: ^1.11.1 - version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3) + version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@8.1.1)(typescript@5.9.3) '@types/gunzip-maybe': specifier: ^1.4.3 version: 1.4.3 @@ -219,7 +219,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -256,7 +256,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -281,7 +281,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -318,13 +318,13 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) date-fns: specifier: ^2.30.0 version: 2.30.0 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@8.1.1) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -343,7 +343,7 @@ importers: dependencies: ava: specifier: ^5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) packages/cli: dependencies: @@ -437,7 +437,7 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) lodash-es: specifier: ^4.17.23 version: 4.17.23 @@ -449,7 +449,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -483,13 +483,13 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -532,7 +532,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) jsonpath: specifier: ^1.3.0 version: 1.3.0 @@ -544,7 +544,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -553,7 +553,7 @@ importers: dependencies: '@typescript/vfs': specifier: ^1.6.4 - version: 1.6.4(typescript@5.9.3) + version: 1.6.4(supports-color@8.1.1)(typescript@5.9.3) cross-fetch: specifier: ^3.2.0 version: 3.2.0(encoding@0.1.13) @@ -578,7 +578,7 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) esbuild: specifier: ^0.18.20 version: 0.18.20 @@ -587,7 +587,7 @@ importers: version: 6.1.3 threads: specifier: 1.7.0 - version: 1.7.0 + version: 1.7.0(supports-color@8.1.1) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -596,7 +596,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) packages/engine-multi: dependencies: @@ -624,7 +624,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -633,7 +633,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -655,10 +655,10 @@ importers: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@8.1.1)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@8.1.1))(supports-color@8.1.1) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -691,7 +691,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@8.1.1) koa-logger: specifier: ^4.0.0 version: 4.0.0 @@ -725,10 +725,10 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) koa-websocket: specifier: ^7.0.0 - version: 7.0.0 + version: 7.0.0(supports-color@8.1.1) query-string: specifier: ^8.2.0 version: 8.2.0 @@ -737,7 +737,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -765,13 +765,13 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -814,7 +814,7 @@ importers: version: 4.13.4 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) mock-fs: specifier: ^5.5.0 version: 5.5.0 @@ -826,7 +826,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -869,7 +869,7 @@ importers: version: 7.7.1 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@8.1.1) mock-fs: specifier: ^5.5.0 version: 5.5.0 @@ -884,7 +884,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -893,10 +893,10 @@ importers: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@5.5.0)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -910,8 +910,8 @@ importers: specifier: workspace:* version: link:../runtime '@sentry/node': - specifier: ^10.44.0 - version: 10.44.0 + specifier: ^11.8.0 + version: 11.8.0(rollup@4.59.0) fast-safe-stringify: specifier: ^2.1.1 version: 2.1.1 @@ -926,7 +926,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@5.5.0) koa-logger: specifier: ^3.2.1 version: 3.2.1 @@ -966,19 +966,19 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@5.5.0) nodemon: specifier: 3.1.14 version: 3.1.14 sentry-testkit: specifier: ^6.2.2 - version: 6.2.2 + version: 6.2.2(supports-color@5.5.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -1367,11 +1367,6 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} - '@fastify/otel@0.17.1': - resolution: {integrity: sha512-K4wyxfUZx2ux5o+b6BtTqouYFVILohLZmSbA2tKUueJstNcBnoGPVhllCaOvbQ3ZrXdUxUC/fyrSWSCqHhdOPg==} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} @@ -1540,209 +1535,134 @@ packages: '@openfn/language-http@6.4.3': resolution: {integrity: sha512-8ihgIYId+ewMuNU9hbe5JWEWvaJInDrIEiy4EyO7tbzu5t/f1kO18JIzQWm6r7dcHiMfcG2QaXe6O3br1xOrDA==} - '@opentelemetry/api-logs@0.207.0': - resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.212.0': - resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.213.0': - resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/context-async-hooks@2.6.0': - resolution: {integrity: sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@2.6.0': - resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/instrumentation-amqplib@0.60.0': - resolution: {integrity: sha512-q/B2IvoVXRm1M00MvhnzpMN6rKYOszPXVsALi6u0ss4AYHe+TidZEtLW9N1ZhrobI1dSriHnBqqtAOZVAv07sg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-connect@0.56.0': - resolution: {integrity: sha512-PKp+sSZ7AfzMvGgO3VCyo1inwNu+q7A1k9X88WK4PQ+S6Hp7eFk8pie+sWHDTaARovmqq5V2osav3lQej2B0nw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-dataloader@0.30.0': - resolution: {integrity: sha512-MXHP2Q38cd2OhzEBKAIXUi9uBlPEYzF6BNJbyjUXBQ6kLaf93kRC41vNMIz0Nl5mnuwK7fDvKT+/lpx7BXRwdg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-express@0.61.0': - resolution: {integrity: sha512-Xdmqo9RZuZlL29Flg8QdwrrX7eW1CZ7wFQPKHyXljNymgKhN1MCsYuqQ/7uxavhSKwAl7WxkTzKhnqpUApLMvQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.32.0': - resolution: {integrity: sha512-koR6apx0g0wX6RRiPpjA4AFQUQUbXrK16kq4/SZjVp7u5cffJhNkY4TnITxcGA4acGSPYAfx3NHRIv4Khn1axQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.56.0': - resolution: {integrity: sha512-fg+Jffs6fqrf0uQS0hom7qBFKsbtpBiBl8+Vkc63Gx8xh6pVh+FhagmiO6oM0m3vyb683t1lP7yGYq22SiDnqg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.61.0': - resolution: {integrity: sha512-pUiVASv6nh2XrerTvlbVHh7vKFzscpgwiQ/xvnZuAIzQ5lRjWVdRPUuXbvZJ/Yq79QsE81TZdJ7z9YsXiss1ew==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.59.0': - resolution: {integrity: sha512-33wa4mEr+9+ztwdgLor1SeBu4Opz4IsmpcLETXAd3VmBrOjez8uQtrsOhPCa5Vhbm5gzDlMYTgFRLQzf8/YHFA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.213.0': - resolution: {integrity: sha512-B978Xsm5XEPGhm1P07grDoaOFLHapJPkOG9h016cJsyWWxmiLnPu2M/4Nrm7UCkHSiLnkXgC+zVGUAIahy8EEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.61.0': - resolution: {integrity: sha512-hsHDadUtAFbws1YSDc1XW0svGFKiUbqv2td1Cby+UAiwvojm1NyBo/taifH0t8CuFZ0x/2SDm0iuTwrM5pnVOg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.22.0': - resolution: {integrity: sha512-wJU4IBQMUikdJAcTChLFqK5lo+flo7pahqd8DSLv7uMxsdOdAHj6RzKYAm8pPfUS6ItKYutYyuicwKaFwQKsoA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-android-arm-eabi@0.152.0': + resolution: {integrity: sha512-393VtirINkae4XHBTkq/yQxhOwEr4V+r0et37P2fp+sYZSdYgZTRDPo5Um8vBh0QjMZ+RmaMy4PP0qwr4cTMwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@opentelemetry/instrumentation-knex@0.57.0': - resolution: {integrity: sha512-vMCSh8kolEm5rRsc+FZeTZymWmIJwc40hjIKnXH4O0Dv/gAkJJIRXCsPX5cPbe0c0j/34+PsENd0HqKruwhVYw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-android-arm64@0.152.0': + resolution: {integrity: sha512-iYFcExATJNZ0rZj0o02THbD1Nc9jAW0nVgIQPee6ejbSASpbZXlIMw8jxtpzaG14JQTWwGSZ6U5dEtRJXE6V3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] - '@opentelemetry/instrumentation-koa@0.61.0': - resolution: {integrity: sha512-lvrfWe9ShK/D2X4brmx8ZqqeWPfRl8xekU0FCn7C1dHm5k6+rTOOi36+4fnaHAP8lig9Ux6XQ1D4RNIpPCt1WQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.9.0 + '@oxc-parser/binding-darwin-arm64@0.152.0': + resolution: {integrity: sha512-fnU+DfUzi5/rFy0LzmipYRz06PAUUkOS8kCJQ3Eg6oqlunC1XZXoffFqxmdL51RmcbYtUG3P8DVG1U5fckoa2A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] - '@opentelemetry/instrumentation-lru-memoizer@0.57.0': - resolution: {integrity: sha512-cEqpUocSKJfwDtLYTTJehRLWzkZ2eoePCxfVIgGkGkb83fMB71O+y4MvRHJPbeV2bdoWdOVrl8uO0+EynWhTEA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-darwin-x64@0.152.0': + resolution: {integrity: sha512-Y3U2okryWTs7hDld6UhjJwLNf8/do+x4g4C+1U5SiGwrTLwbO83WJYJA8+l9iFSilWBgnfpd/+U76tfeNxUHzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] - '@opentelemetry/instrumentation-mongodb@0.66.0': - resolution: {integrity: sha512-d7m9QnAY+4TCWI4q1QRkfrc6fo/92VwssaB1DzQfXNRvu51b78P+HJlWP7Qg6N6nkwdb9faMZNBCZJfftmszkw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-freebsd-x64@0.152.0': + resolution: {integrity: sha512-XkhkRrrPffEw+ZLv/d/5pEni0buxinm/E/N6fLbJCunD1DBbeCTtVP23ESV+4CWCWdSz397asks1ViGslpmWiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] - '@opentelemetry/instrumentation-mongoose@0.59.0': - resolution: {integrity: sha512-6/jWU+c1NgznkVLDU/2y0bXV2nJo3o9FWZ9mZ9nN6T/JBNRoMnVXZl2FdBmgH+a5MwaWLs5kmRJTP5oUVGIkPw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + resolution: {integrity: sha512-MLzsRRbdfmdzo6JEhbC1yKU9ItfvM8tEf18HM32jCiNYZEz6J3bRBRl3cTkKR0vBdrk9ZVlcARXtwHccs+epDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - '@opentelemetry/instrumentation-mysql2@0.59.0': - resolution: {integrity: sha512-n9/xrVCRBfG9egVbffnlU1uhr+HX0vF4GgtAB/Bvm48wpFgRidqD8msBMiym1kRYzmpWvJqTxNT47u1MkgBEdw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + resolution: {integrity: sha512-HHerTEJz7Iy10Ji6BGsk4gqG6PU0UzFuBu91jIEOX89FnrI5ECWSEr7BLpkYQhp7U0QFtMnBhNfQKI0hYZK78w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] - '@opentelemetry/instrumentation-mysql@0.59.0': - resolution: {integrity: sha512-r+V/Fh0sm7Ga8/zk/TI5H5FQRAjwr0RrpfPf8kNIehlsKf12XnvIaZi8ViZkpX0gyPEpLXqzqWD6QHlgObgzZw==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + resolution: {integrity: sha512-9Bb8GyzZ2uSLzRDZ27usaKokr07mxiBe2dDF5b6bd2janmaJTT1gL51UfP7AU30yFiXrJvcRyFKREAgKwZYs0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-pg@0.65.0': - resolution: {integrity: sha512-W0zpHEIEuyZ8zvb3njaX9AAbHgPYOsSWVOoWmv1sjVRSF6ZpBqtlxBWbU+6hhq1TFWBeWJOXZ8nZS/PUFpLJYQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + resolution: {integrity: sha512-om7iDMUroI74A/lfRRWY9/Ev3VWrVsrUZ4tyvyh8raBey/sdcfyAD7tYWc+iykAiotnmftyKTpUKFr1QnaBrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] - '@opentelemetry/instrumentation-redis@0.61.0': - resolution: {integrity: sha512-JnPexA034/0UJRsvH96B0erQoNOqKJZjE2ZRSw9hiTSC23LzE0nJE/u6D+xqOhgUhRnhhcPHq4MdYtmUdYTF+Q==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + resolution: {integrity: sha512-Lf1OL2lMTqwDMFC6jnRb30B+WYHKV0XT+bPHcFM6xHtNVwxpobiTzUIvKH6a0LWpvfwyru8Yu6zCGHqqFHH+gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-tedious@0.32.0': - resolution: {integrity: sha512-BQS6gG8RJ1foEqfEZ+wxoqlwfCAzb1ZVG0ad8Gfe4x8T658HJCLGLd4E4NaoQd8EvPfLqOXgzGaE/2U4ytDSWA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + resolution: {integrity: sha512-BYWSMQK+YJ9ZILStPcmmiSVq8KEG9Qdq/c4BHPC3rUEq/fEDCVjUIwdj5ZzjgZ9P9is9PKu0tcAXzq1ZXg4urQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation-undici@0.23.0': - resolution: {integrity: sha512-LL0VySzKVR2cJSFVZaTYpZl1XTpBGnfzoQPe2W7McS2267ldsaEIqtQY6VXs2KCXN0poFjze5110PIpxHDaDGg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.7.0 + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + resolution: {integrity: sha512-d2LqHNNNqdH185sRK5dPKWZu201JrNu9/CRFuVEuT+ht7n/QaVblEMWT9MALQPrDWYvhG10P+NSKJFquDWZ09w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] - '@opentelemetry/instrumentation@0.207.0': - resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + resolution: {integrity: sha512-8gBeHt5rA/8+pKhHl+zLggfa3L/V/L2at96p7ty6pBE7id6lFO19nFUc15u2PcjC4OYeYK465MKy1fhhbpFIGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation@0.212.0': - resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + resolution: {integrity: sha512-ltgrSF6L+yPTVsIdaAyqKq3zTqzj/1VlNbxWZuxAe8flR3sWdyNsKvdXukoO8/ucJbLickhmieRBge+o/e8Ugw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] - '@opentelemetry/instrumentation@0.213.0': - resolution: {integrity: sha512-3i9NdkET/KvQomeh7UaR/F4r9P25Rx6ooALlWXPIjypcEOUxksCmVu0zA70NBJWlrMW1rPr/LRidFAflLI+s/w==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 + '@oxc-parser/binding-linux-x64-musl@0.152.0': + resolution: {integrity: sha512-LcMp6BvrBnewSfdkLHoSqidhoAq4syKOTBV5TYHWd+2VnGkfD/nPRRVIzTBt/Lkbt7pfOFDGtkiTntFS0p6sQQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] - '@opentelemetry/redis-common@0.38.2': - resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} - engines: {node: ^18.19.0 || >=20.6.0} + '@oxc-parser/binding-openharmony-arm64@0.152.0': + resolution: {integrity: sha512-jbhoMCslVxzRCRNQ8TSHxjYdSKRvk8O6ciLVDkahJvmyHDzBqba/bMQHM6ULKj/rgHNNbSzlIiakdx8SEfkJzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] - '@opentelemetry/resources@2.6.0': - resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + resolution: {integrity: sha512-JZYVlhv36mr/nertnRB7vchJECOBb1Lz6lltalILsB+o9M/is+H6BEDRLQKdqA4JKs6NjJszo46+iM5sfwj6jQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] - '@opentelemetry/sdk-trace-base@2.6.0': - resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + resolution: {integrity: sha512-1X9oganvVsunRH0lH10UU05N+H9atpX6WsOybByUjwnfcd3h/0tYwvuv16XuHglrIUOnxV2TodbLVEwwafd64g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] - '@opentelemetry/semantic-conventions@1.40.0': - resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} - engines: {node: '>=14'} + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + resolution: {integrity: sha512-sw0M80/dn9rcpu+Rqqph/C+MjvDDce6MlXmFjsNQtPEymha0eJy3ml6+foSUTqFfpDIDyHZZ4eoJ43IoMe8bJw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] - '@opentelemetry/sql-common@0.41.2': - resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.1.0 + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -1852,11 +1772,6 @@ packages: cpu: [x64] os: [win32] - '@prisma/instrumentation@7.4.2': - resolution: {integrity: sha512-r9JfchJF1Ae6yAxcaLu/V1TGqBhAuSDe3mRNOssBfx1rMzfZ4fdNvrgUBwyb/TNTGXFxlH9AZix5P257x07nrg==} - peerDependencies: - '@opentelemetry/api': ^1.8 - '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -1995,50 +1910,46 @@ packages: cpu: [x64] os: [win32] - '@sentry/core@10.44.0': - resolution: {integrity: sha512-aa7CiDaNFZvHpqd97LJhuskolfJ/4IH5xyuVVLnv7l6B0v9KTwskPUxb0tH1ej3FxuzfH+i8iTiTFuqpfHS3QA==} - engines: {node: '>=18'} - - '@sentry/node-core@10.44.0': - resolution: {integrity: sha512-jUGsadMrvZ08UMbqJBfjFFMk1k3VbyxfUypf0iDGGgyLmuHotYQPo/5aND+o2KxMDXR60LwcQrMoZFpanK6jXQ==} - engines: {node: '>=18'} + '@sentry/bundler-plugins@11.8.0': + resolution: {integrity: sha512-rQrbL2lYPyiiO6ngdpbbfPKyEzVVQsz1FG+aE8jUiUYs72gvmDmZmRyl9VDCGLrHZfIEwaQMT1t6WQH5nopu5A==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/instrumentation': '>=0.57.1 <1' - '@opentelemetry/resources': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@opentelemetry/semantic-conventions': ^1.39.0 + '@babel/core': ^7.18.5 + rollup: '>=3.2.0' + webpack: '>=5.1.0' peerDependenciesMeta: - '@opentelemetry/api': + '@babel/core': optional: true - '@opentelemetry/context-async-hooks': + rollup: optional: true - '@opentelemetry/core': - optional: true - '@opentelemetry/instrumentation': - optional: true - '@opentelemetry/resources': - optional: true - '@opentelemetry/sdk-trace-base': - optional: true - '@opentelemetry/semantic-conventions': + webpack: optional: true - '@sentry/node@10.44.0': - resolution: {integrity: sha512-q+/WR9ZeF9Af8uyehOj2tQQOa7LH07mJfOuDus5X6G6cLuugdRUGUBB5Qhw+J/ULSxbzGADBZv6AYOyoGaNx7w==} - engines: {node: '>=18'} + '@sentry/conventions@0.27.0': + resolution: {integrity: sha512-Mrqd/o7n/z7qWSYfQQA7x4fFTA0IqOG6JNsbBpnC97khPwZk7n6DPtOO7YgaYplSl8R8QLRbBz9Xv2lVhToSAg==} + engines: {node: '>=14'} - '@sentry/opentelemetry@10.44.0': - resolution: {integrity: sha512-zP4vP8tBxjlmxQ4VcWOwZ0b3lPUxlYPg9FqJwANm9SRJN+7V5psm8TIaAtu9uqtIcJMRHdXkOM4cAggNiLk0KA==} - engines: {node: '>=18'} + '@sentry/core@11.8.0': + resolution: {integrity: sha512-4MVTthk6C/1oQFjbVIfygHyNkx2YGbwNLml0kJji0vjJXy6ZNls9zdIp0FTBTi4e3189IcSc5OXD4qcJdmEeKA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/node@11.8.0': + resolution: {integrity: sha512-8vFPfCRYoUYmsno+OYdvtf5akIUUvd7FvcuIGcfrge8S/bSLFRKq58YtFmX25JFcrMNZ2oQD8r0jkgZfm/2iuA==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/opentelemetry@11.8.0': + resolution: {integrity: sha512-wKuG2SbHHiGDT/fgT4X5n2usO4AjVkYfAj41002mZ18JZU3mNLeCq+qw/nnDIYbcsfIMOUm/eMh3ShKCE+DpXQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.1.0 - '@opentelemetry/core': ^1.30.1 || ^2.1.0 - '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@opentelemetry/semantic-conventions': ^1.39.0 + + '@sentry/server-runtime-injection@11.8.0': + resolution: {integrity: sha512-4uB4E8+9ni5NcJ463aYK/+n1hTMw/vSt4s+AkQ84CKigRDwv2FNNnD1AWpipWNj/xEQ6eQbUitGl7wg7/Ri8Pg==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} + + '@sentry/server-utils@11.8.0': + resolution: {integrity: sha512-2MRFz1rXxnuDGafl6o/itwKHZeZn5hUZYeXNbQK012uO/a2pXhEc8Zh3FeO5L4tjGrIWHx419saAFnKwbVC2IQ==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0'} '@slack/logger@4.0.1': resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} @@ -2241,9 +2152,6 @@ packages: '@types/mute-stream@0.0.4': resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/mysql@2.15.27': - resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node-localstorage@1.3.3': resolution: {integrity: sha512-Wkn5g4eM5x10UNV9Xvl9K6y6m0zorocuJy4WjB5muUdyMZuPbZpSJG3hlhjGHe1HGxbOQO7RcB+jlHcNwkh+Jw==} @@ -2265,12 +2173,6 @@ packages: '@types/nodemon@1.19.3': resolution: {integrity: sha512-LcKdWgch8uHOF73yYpdE7YPVLT0HnFI60zyNBpJyfAiDDwPy3WAxReQeB84UseE8e8qdJsBqmFXWbjxv7jlXBg==} - '@types/pg-pool@2.0.7': - resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} - - '@types/pg@8.15.6': - resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} - '@types/phoenix@1.6.7': resolution: {integrity: sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==} @@ -2301,9 +2203,6 @@ packages: '@types/tar-stream@2.2.3': resolution: {integrity: sha512-if3mugZfjVkXOMZdFjIHySxY13r6GXPpyOlsDmLffvyI7tLz9wXE8BFjNivXsvUeyJ1KNlOpfLnag+ISmxgxPw==} - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - '@types/treeify@1.0.3': resolution: {integrity: sha512-hx0o7zWEUU4R2Amn+pjCBQQt23Khy/Dk56gQU5xi5jtPL1h83ACJCeFaB2M/+WO1AntvWrSoVnnCAfI1AQH4Cg==} @@ -2328,11 +2227,6 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -2609,9 +2503,6 @@ packages: ci-parallel-vars@1.0.1: resolution: {integrity: sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==} - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} engines: {node: '>=12'} @@ -2833,6 +2724,10 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dreamopt@0.8.0: resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} engines: {node: '>=0.4.0'} @@ -3185,9 +3080,6 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3333,13 +3225,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-in-the-middle@2.0.6: - resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} - - import-in-the-middle@3.0.0: - resolution: {integrity: sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg==} - engines: {node: '>=18'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -3656,9 +3541,6 @@ packages: resolution: {integrity: sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==} engines: {node: '>=12.0.0'} - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3746,6 +3628,10 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxc-parser@0.152.0: + resolution: {integrity: sha512-ZztsZgaCWmPeKYMdoTT5e0oV/oxD1NeLWNR3kAtr/G707cUPYaaozRlVDHLCGnszHJRszN9mJwmPNpqgwloC0A==} + engines: {node: ^20.19.0 || >=22.12.0} + oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} @@ -3867,17 +3753,6 @@ packages: peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - phoenix@1.7.10: resolution: {integrity: sha512-akfr/QvLPFRB8sORyc8FQFY/YoGwjWhka/YRcu45sKlBOZHvA80EkLYBUsYlW63UicxgrXABZdrjDkv54LTE+g==} @@ -3936,22 +3811,6 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -4034,10 +3893,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.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'} - resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -4099,6 +3954,11 @@ packages: sentry-testkit@6.2.2: resolution: {integrity: sha512-1YdDokVhf8pbCGF91TeaWP1ue+lHSB358nH+I3mGOWg79O1hssvv3H3OWMojp9RZ+qHXm0DgcZ05oIjnbYoMRw==} + sentry@0.45.0: + resolution: {integrity: sha512-kgmEbLlC22lL7uneeUm9Sjz65rp5cvwYOMTxXbZCmBum4sbVre/lLT3zTV3kwXHJ9/BM8ZS0/GhZe9Cl1t6WyA==} + engines: {node: '>=20.0'} + hasBin: true + serialize-error@7.0.1: resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} engines: {node: '>=10'} @@ -4246,6 +4106,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} @@ -4860,16 +4724,6 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@fastify/otel@0.17.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - minimatch: 10.2.4 - transitivePeerDependencies: - - supports-color - '@hapi/bourne@3.0.0': {} '@inquirer/checkbox@1.5.2': @@ -5041,19 +4895,37 @@ snapshots: dependencies: jsep: 1.4.0 - '@koa/bodyparser@6.1.0(koa@2.16.4)': + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@5.5.0))': dependencies: '@types/co-body': 6.1.3 co-body: 6.2.0 - koa: 2.16.4 + koa: 2.16.4(supports-color@5.5.0) lodash.merge: 4.6.2 type-is: 2.0.1 - '@koa/router@15.4.0(koa@2.16.4)': + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@8.1.1))': + dependencies: + '@types/co-body': 6.1.3 + co-body: 6.2.0 + koa: 2.16.4(supports-color@8.1.1) + lodash.merge: 4.6.2 + type-is: 2.0.1 + + '@koa/router@15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0)': dependencies: debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 - koa: 2.16.4 + koa: 2.16.4(supports-color@5.5.0) + koa-compose: 4.1.0 + path-to-regexp: 8.4.1 + transitivePeerDependencies: + - supports-color + + '@koa/router@15.4.0(koa@2.16.4(supports-color@8.1.1))(supports-color@8.1.1)': + dependencies: + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.1 + koa: 2.16.4(supports-color@8.1.1) koa-compose: 4.1.0 path-to-regexp: 8.4.1 transitivePeerDependencies: @@ -5156,268 +5028,66 @@ snapshots: cheerio: 1.0.0-rc.12 cheerio-tableparser: 1.0.1 - '@opentelemetry/api-logs@0.207.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.212.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api-logs@0.213.0': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} + '@opentelemetry/api@1.9.1': {} - '@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.40.0 - - '@opentelemetry/instrumentation-amqplib@0.60.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-connect@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.30.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.56.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - forwarded-parse: 2.1.2 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-ioredis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-kafkajs@0.22.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-android-arm-eabi@0.152.0': + optional: true - '@opentelemetry/instrumentation-knex@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-android-arm64@0.152.0': + optional: true - '@opentelemetry/instrumentation-koa@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-darwin-arm64@0.152.0': + optional: true - '@opentelemetry/instrumentation-lru-memoizer@0.57.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-darwin-x64@0.152.0': + optional: true - '@opentelemetry/instrumentation-mongodb@0.66.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-freebsd-x64@0.152.0': + optional: true - '@opentelemetry/instrumentation-mongoose@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm-gnueabihf@0.152.0': + optional: true - '@opentelemetry/instrumentation-mysql2@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm-musleabihf@0.152.0': + optional: true - '@opentelemetry/instrumentation-mysql@0.59.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/mysql': 2.15.27 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-pg@0.65.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.6 - '@types/pg-pool': 2.0.7 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-arm64-musl@0.152.0': + optional: true - '@opentelemetry/instrumentation-redis@0.61.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-ppc64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-tedious@0.32.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-riscv64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation-undici@0.23.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-riscv64-musl@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-s390x-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.212.0 - import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-x64-gnu@0.152.0': + optional: true - '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.213.0 - import-in-the-middle: 3.0.0 - require-in-the-middle: 8.0.1 - transitivePeerDependencies: - - supports-color + '@oxc-parser/binding-linux-x64-musl@0.152.0': + optional: true - '@opentelemetry/redis-common@0.38.2': {} + '@oxc-parser/binding-openharmony-arm64@0.152.0': + optional: true - '@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 + '@oxc-parser/binding-win32-arm64-msvc@0.152.0': + optional: true - '@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 + '@oxc-parser/binding-win32-ia32-msvc@0.152.0': + optional: true - '@opentelemetry/semantic-conventions@1.40.0': {} + '@oxc-parser/binding-win32-x64-msvc@0.152.0': + optional: true - '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) + '@oxc-project/types@0.152.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -5481,13 +5151,6 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -5563,70 +5226,54 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@sentry/core@10.44.0': {} - - '@sentry/node-core@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/bundler-plugins@11.8.0(rollup@4.59.0)': dependencies: - '@sentry/core': 10.44.0 - '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - import-in-the-middle: 3.0.0 + '@sentry/core': 11.8.0 + dotenv: 17.4.2 + glob: 13.0.6 + magic-string: 0.30.21 + oxc-parser: 0.152.0 + sentry: 0.45.0 + supports-color: 8.1.1 optionalDependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - - '@sentry/node@10.44.0': - dependencies: - '@fastify/otel': 0.17.1(@opentelemetry/api@1.9.0) - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.60.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.30.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.66.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.65.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.23.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@prisma/instrumentation': 7.4.2(@opentelemetry/api@1.9.0) - '@sentry/core': 10.44.0 - '@sentry/node-core': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) - import-in-the-middle: 3.0.0 + rollup: 4.59.0 + + '@sentry/conventions@0.27.0': {} + + '@sentry/core@11.8.0': + dependencies: + '@sentry/conventions': 0.27.0 + + '@sentry/node@11.8.0(rollup@4.59.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/bundler-plugins': 11.8.0(rollup@4.59.0) + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 + '@sentry/opentelemetry': 11.8.0(@opentelemetry/api@1.9.1) + '@sentry/server-runtime-injection': 11.8.0 + '@sentry/server-utils': 11.8.0 transitivePeerDependencies: - - supports-color + - '@babel/core' + - rollup + - webpack - '@sentry/opentelemetry@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/opentelemetry@11.8.0(@opentelemetry/api@1.9.1)': dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/core': 10.44.0 + '@opentelemetry/api': 1.9.1 + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 + + '@sentry/server-runtime-injection@11.8.0': + dependencies: + '@sentry/core': 11.8.0 + '@sentry/server-utils': 11.8.0 + + '@sentry/server-utils@11.8.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/conventions': 0.27.0 + '@sentry/core': 11.8.0 '@slack/logger@4.0.1': dependencies: @@ -5634,13 +5281,13 @@ snapshots: '@slack/types@2.21.1': {} - '@slack/web-api@7.16.0': + '@slack/web-api@7.16.0(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@slack/logger': 4.0.1 '@slack/types': 2.21.1 '@types/node': 25.6.0 '@types/retry': 0.12.0 - axios: 1.16.1 + axios: 1.16.1(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1) eventemitter3: 5.0.4 form-data: 4.0.5 is-electron: 2.2.2 @@ -5657,13 +5304,13 @@ snapshots: '@swc/core': 1.15.18 '@swc/types': 0.1.25 - '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3)': + '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@8.1.1)(typescript@5.9.3)': dependencies: '@swc-node/core': 1.14.1(@swc/core@1.15.18)(@swc/types@0.1.25) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.18 colorette: 2.0.20 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) oxc-resolver: 11.19.1 pirates: 4.0.7 tslib: 2.8.1 @@ -5849,10 +5496,6 @@ snapshots: dependencies: '@types/node': 18.19.130 - '@types/mysql@2.15.27': - dependencies: - '@types/node': 18.19.130 - '@types/node-localstorage@1.3.3': dependencies: '@types/node': 18.19.130 @@ -5879,16 +5522,6 @@ snapshots: dependencies: '@types/node': 18.19.130 - '@types/pg-pool@2.0.7': - dependencies: - '@types/pg': 8.15.6 - - '@types/pg@8.15.6': - dependencies: - '@types/node': 18.19.130 - pg-protocol: 1.13.0 - pg-types: 2.2.0 - '@types/phoenix@1.6.7': {} '@types/qs@6.15.0': {} @@ -5919,10 +5552,6 @@ snapshots: dependencies: '@types/node': 25.5.0 - '@types/tedious@4.0.14': - dependencies: - '@types/node': 18.19.130 - '@types/treeify@1.0.3': {} '@types/wrap-ansi@3.0.0': {} @@ -5937,9 +5566,9 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/vfs@1.6.4(typescript@5.9.3)': + '@typescript/vfs@1.6.4(supports-color@8.1.1)(typescript@5.9.3)': dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5949,19 +5578,15 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-walk@8.3.5: dependencies: acorn: 8.16.0 acorn@8.16.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -6039,7 +5664,7 @@ snapshots: asynckit@0.4.0: {} - ava@5.3.1: + ava@5.3.1(supports-color@5.5.0): dependencies: acorn: 8.16.0 acorn-walk: 8.3.5 @@ -6087,11 +5712,59 @@ snapshots: transitivePeerDependencies: - supports-color - axios@1.16.1: + ava@5.3.1(supports-color@8.1.1): + dependencies: + acorn: 8.16.0 + acorn-walk: 8.3.5 + ansi-styles: 6.2.3 + arrgv: 1.0.2 + arrify: 3.0.0 + callsites: 4.2.0 + cbor: 8.1.0 + chalk: 5.6.2 + chokidar: 3.6.0 + chunkd: 2.0.1 + ci-info: 3.9.0 + ci-parallel-vars: 1.0.1 + clean-yaml-object: 0.1.0 + cli-truncate: 3.1.0 + code-excerpt: 4.0.0 + common-path-prefix: 3.0.0 + concordance: 5.0.4 + currently-unhandled: 0.4.1 + debug: 4.4.3(supports-color@8.1.1) + emittery: 1.2.1 + figures: 5.0.0 + globby: 13.2.2 + ignore-by-default: 2.1.0 + indent-string: 5.0.0 + is-error: 2.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + matcher: 5.0.0 + mem: 9.0.2 + ms: 2.1.3 + p-event: 5.0.1 + p-map: 5.5.0 + picomatch: 2.3.1 + pkg-conf: 4.0.0 + plur: 5.1.0 + pretty-ms: 8.0.0 + resolve-cwd: 3.0.0 + stack-utils: 2.0.6 + strip-ansi: 7.2.0 + supertap: 3.0.1 + temp-dir: 3.0.0 + write-file-atomic: 5.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + axios@1.16.1(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) form-data: 4.0.5 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -6142,11 +5815,11 @@ snapshots: blueimp-md5@2.19.0: {} - body-parser@1.20.4: + body-parser@1.20.4(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -6267,8 +5940,6 @@ snapshots: ci-parallel-vars@1.0.1: {} - cjs-module-lexer@2.2.0: {} - clean-stack@4.2.0: dependencies: escape-string-regexp: 5.0.0 @@ -6404,9 +6075,11 @@ snapshots: dependencies: time-zone: 1.0.0 - debug@2.6.9: + debug@2.6.9(supports-color@5.5.0): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 5.5.0 debug@4.4.3(supports-color@5.5.0): dependencies: @@ -6414,6 +6087,12 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + decode-uri-component@0.4.1: {} deep-equal@1.0.1: {} @@ -6462,6 +6141,8 @@ snapshots: dotenv@17.3.1: {} + dotenv@17.4.2: {} + dreamopt@0.8.0: dependencies: wordwrap: 1.0.0 @@ -6706,21 +6387,21 @@ snapshots: transitivePeerDependencies: - bare-abort-controller - express@4.22.1: + express@4.22.1(supports-color@5.5.0): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.4(supports-color@5.5.0) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@5.5.0) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -6732,8 +6413,8 @@ snapshots: qs: 6.14.2 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@5.5.0) + serve-static: 1.16.3(supports-color@5.5.0) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -6793,9 +6474,9 @@ snapshots: filter-obj@5.1.0: {} - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -6821,7 +6502,9 @@ snapshots: mlly: 1.8.1 rollup: 4.59.0 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) form-data@4.0.5: dependencies: @@ -6831,8 +6514,6 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - forwarded-parse@2.1.2: {} - forwarded@0.2.0: {} fresh@0.5.2: {} @@ -6966,10 +6647,10 @@ snapshots: http-status-codes@2.3.0: {} - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3(supports-color@5.5.0) + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -6996,20 +6677,6 @@ snapshots: ignore@5.3.2: {} - import-in-the-middle@2.0.6: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - - import-in-the-middle@3.0.0: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -7155,10 +6822,10 @@ snapshots: on-finished: 2.4.1 passthrough-counter: 1.0.0 - koa-websocket@7.0.0: + koa-websocket@7.0.0(supports-color@8.1.1): dependencies: co: 4.6.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) koa-compose: 4.1.0 ws: 8.19.0 transitivePeerDependencies: @@ -7166,7 +6833,7 @@ snapshots: - supports-color - utf-8-validate - koa@2.16.4: + koa@2.16.4(supports-color@5.5.0): dependencies: accepts: 1.3.8 cache-content-type: 1.0.1 @@ -7194,6 +6861,34 @@ snapshots: transitivePeerDependencies: - supports-color + koa@2.16.4(supports-color@8.1.1): + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + debug: 4.4.3(supports-color@8.1.1) + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.1.2 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7295,8 +6990,6 @@ snapshots: mock-fs@5.5.0: {} - module-details-from-path@1.0.4: {} - mri@1.2.0: {} ms@2.0.0: {} @@ -7367,6 +7060,30 @@ snapshots: outdent@0.5.0: {} + oxc-parser@0.152.0: + dependencies: + '@oxc-project/types': 0.152.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.152.0 + '@oxc-parser/binding-android-arm64': 0.152.0 + '@oxc-parser/binding-darwin-arm64': 0.152.0 + '@oxc-parser/binding-darwin-x64': 0.152.0 + '@oxc-parser/binding-freebsd-x64': 0.152.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.152.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.152.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.152.0 + '@oxc-parser/binding-linux-arm64-musl': 0.152.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.152.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.152.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-gnu': 0.152.0 + '@oxc-parser/binding-linux-x64-musl': 0.152.0 + '@oxc-parser/binding-openharmony-arm64': 0.152.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.152.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.152.0 + '@oxc-parser/binding-win32-x64-msvc': 0.152.0 + oxc-resolver@11.19.1: optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 @@ -7490,18 +7207,6 @@ snapshots: duplexify: 3.7.1 through2: 2.0.5 - pg-int8@1.0.1: {} - - pg-protocol@1.13.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - phoenix@1.7.10: {} phoenix@1.8.5: {} @@ -7547,16 +7252,6 @@ snapshots: source-map-js: 1.2.1 optional: true - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - prettier@2.8.8: {} pretty-ms@8.0.0: @@ -7646,13 +7341,6 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color - resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -7721,9 +7409,9 @@ snapshots: semver@7.7.4: {} - send@0.19.2: + send@0.19.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -7739,23 +7427,25 @@ snapshots: transitivePeerDependencies: - supports-color - sentry-testkit@6.2.2: + sentry-testkit@6.2.2(supports-color@5.5.0): dependencies: - body-parser: 1.20.4 - express: 4.22.1 + body-parser: 1.20.4(supports-color@5.5.0) + express: 4.22.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color + sentry@0.45.0: {} + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 - serve-static@1.16.3: + serve-static@1.16.3(supports-color@5.5.0): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7911,6 +7601,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + tar-stream@3.1.8: dependencies: b4a: 1.8.0 @@ -7947,10 +7641,10 @@ snapshots: dependencies: any-promise: 1.3.0 - threads@1.7.0: + threads@1.7.0(supports-color@8.1.1): dependencies: callsites: 3.1.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -8025,7 +7719,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -8054,13 +7748,42 @@ snapshots: - tsx - yaml - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@8.1.1) + esbuild: 0.27.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.15.18 + postcss: 8.5.8 + typescript: 4.9.5 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@8.1.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3(supports-color@8.1.1) esbuild: 0.27.4 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 From e85c79da8734b8f4dce167d43bd45ca0b72990c4 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 15:06:13 +0200 Subject: [PATCH 08/12] Replace mock-fs with memfs in unit tests mock-fs 5.5.0 crashes on Node 26.12.0. Each package that used it gets a test/helpers/memfs.ts that swaps node:fs and node:fs/promises for memfs, leaving the repo's node_modules on the real disk so lazy requires still work. --- packages/cli/package.json | 3 +- packages/cli/test/commands.test.ts | 31 +- packages/cli/test/compile/compile.test.ts | 30 +- packages/cli/test/compile/handler.test.ts | 2 +- packages/cli/test/deploy/deploy.test.ts | 22 +- packages/cli/test/docgen/handler.test.ts | 22 +- packages/cli/test/env.test.ts | 14 +- packages/cli/test/execute/execute.test.ts | 2 +- .../cli/test/execute/serialize-output.test.ts | 4 +- packages/cli/test/helpers/memfs.ts | 119 +++ packages/cli/test/projects/checkout.test.ts | 24 +- packages/cli/test/projects/clean.test.ts | 12 +- .../test/projects/credentials-helpers.test.ts | 16 +- packages/cli/test/projects/deploy.test.ts | 29 +- .../cli/test/projects/ensure-checkout.test.ts | 6 +- packages/cli/test/projects/fetch.test.ts | 18 +- packages/cli/test/projects/list.test.ts | 4 +- packages/cli/test/projects/merge.test.ts | 10 +- packages/cli/test/projects/util.test.ts | 10 +- packages/cli/test/pull/handler.test.ts | 18 +- packages/cli/test/util.ts | 14 +- packages/cli/test/util/file-exists.test.ts | 10 +- packages/cli/test/util/load-plan.test.ts | 38 +- .../util/map-adaptors-to-monorepo.test.ts | 30 +- packages/cli/test/util/print-versions.test.ts | 18 +- .../cli/test/util/validate-adaptors.test.ts | 4 +- packages/deploy/package.json | 3 +- packages/deploy/test/helpers/memfs.ts | 119 +++ packages/deploy/test/validator.test.ts | 6 +- packages/project/package.json | 3 +- packages/project/test/helpers/memfs.ts | 119 +++ packages/project/test/parse/from-fs.test.ts | 6 +- packages/project/test/parse/from-path.test.ts | 14 +- packages/project/test/util/config.test.ts | 20 +- packages/project/test/workspace.test.ts | 4 +- packages/runtime/package.json | 3 +- packages/runtime/test/helpers/memfs.ts | 119 +++ .../runtime/test/repo/ensure-repo.test.ts | 14 +- .../test/repo/get-module-entry-point.test.ts | 6 +- .../runtime/test/repo/get-module-path.test.ts | 6 +- packages/runtime/test/repo/install.test.ts | 8 +- pnpm-lock.yaml | 824 +++++++++++++----- 42 files changed, 1315 insertions(+), 469 deletions(-) create mode 100644 packages/cli/test/helpers/memfs.ts create mode 100644 packages/deploy/test/helpers/memfs.ts create mode 100644 packages/project/test/helpers/memfs.ts create mode 100644 packages/runtime/test/helpers/memfs.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 5c2b1987d..76b3a622d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,7 +40,6 @@ "@openfn/language-common": "3.2.3", "@types/json-diff": "^1.0.3", "@types/lodash-es": "~4.17.12", - "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.130", "@types/rimraf": "^3.0.2", "@types/treeify": "^1.0.3", @@ -48,7 +47,7 @@ "@types/yargs": "^17.0.35", "ava": "5.3.1", "lodash-es": "^4.17.23", - "mock-fs": "^5.5.0", + "memfs": "^4.80.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/cli/test/commands.test.ts b/packages/cli/test/commands.test.ts index 823a6b9cc..34442b193 100644 --- a/packages/cli/test/commands.test.ts +++ b/packages/cli/test/commands.test.ts @@ -1,6 +1,6 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs, restoreFs } from './helpers/memfs'; import { execSync } from 'node:child_process'; import fs from 'node:fs/promises'; import os from 'node:os'; @@ -20,7 +20,7 @@ const TIMEOUT = 1000 * 30; const logger = createMockLogger('', { level: 'debug' }); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); @@ -58,26 +58,19 @@ async function run(command: string, job: string, options: RunOptions = {}) { const state = JSON.stringify(options.state) || '{ "data": {}, "configuration": {} }'; - // Ensure that pnpm is not mocked out - // This is needed to ensure that pnpm dependencies can be dynamically loaded - // (for recast in particular) - const pnpm = path.resolve('../../node_modules/.pnpm'); - const pkgPath = path.resolve('./package.json'); - const recastPath = `${pnpm}/recast@0.21.5`; // Mock the file system in-memory if (!options.disableMock) { - mock({ + mockFs({ [expressionPath]: job, [statePath]: state, [outputPath]: '{}', - [recastPath]: mock.load(recastPath, {}), // enable us to load test modules through the mock - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), - '/monorepo/': mock.load(path.resolve('test/__monorepo__/'), {}), - //'node_modules': mock.load(path.resolve('node_modules/'), {}), - [pkgPath]: mock.load(pkgPath), + '/modules/': loadPath(path.resolve('test/__modules__/')), + '/repo/': loadPath(path.resolve('test/__repo__/')), + '/monorepo/': loadPath(path.resolve('test/__monorepo__/')), + //'node_modules': loadPath(path.resolve('node_modules/')), + [pkgPath]: loadPath(pkgPath), ...(options.mockfs ?? {}), }); } @@ -732,7 +725,7 @@ test.serial('compile a workflow: openfn compile wf.json to file', async (t) => { test.serial('docs should print documentation with full names', async (t) => { t.timeout(TIMEOUT); - mock({ + mockFs({ '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', functions: [ @@ -764,8 +757,8 @@ test.serial('docs should print documentation with full names', async (t) => { test.serial('docs adaptor should list operations', async (t) => { t.timeout(TIMEOUT); const pkgPath = path.resolve('./package.json'); - mock({ - [pkgPath]: mock.load(pkgPath), + mockFs({ + [pkgPath]: loadPath(pkgPath), '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', version: '1.0.0', @@ -794,7 +787,7 @@ test.serial( 'docs adaptor + operation should print documentation with shorthand names', async (t) => { t.timeout(TIMEOUT); - mock({ + mockFs({ '/repo/docs/@openfn/language-common@1.0.0.json': JSON.stringify({ name: 'test', functions: [ diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index cab8e333b..72ea84402 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs as mockMemFs, restoreFs } from '../helpers/memfs'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createMockLogger } from '@openfn/logger'; @@ -101,7 +101,7 @@ steps: const code = await fs.readFile(outPaths[0], 'utf-8'); t.true(code.includes('export default [x()]')); - mock.restore(); + restoreFs(); }); test.serial( @@ -132,7 +132,7 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].endsWith('step-a.mjs')); - mock.restore(); + restoreFs(); } ); @@ -162,7 +162,7 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/proj/build/')); - mock.restore(); + restoreFs(); } ); @@ -199,7 +199,7 @@ steps: t.is(outPaths.length, 1); await t.throwsAsync(() => fs.access('/proj/dist/old-workflow/stale.mjs')); - mock.restore(); + restoreFs(); } ); @@ -226,7 +226,7 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/out/')); - mock.restore(); + restoreFs(); }); test.serial('throw an AbortError if a job is uncompilable', async (t) => { @@ -314,7 +314,7 @@ test.serial('loadTransformOptions: do nothing', async (t) => { test.serial( "resolveSpecifierPath: return null if the module can't be resolved locally", async (t) => { - mock({ + mockMemFs({ '/repo': {}, }); const path = await resolveSpecifierPath('pkg', '/repo', mockLog); @@ -360,7 +360,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return a path to the repo if the module is found', async (t) => { - mock({ + mockMemFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -376,7 +376,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return null if a module is not in the repo', async (t) => { - mock({ + mockMemFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -392,8 +392,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path', async (t) => { - mock({ - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), + mockMemFs({ + '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { // This should find the times-two module in test/__modules__ @@ -416,8 +416,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path and version specifier', async (t) => { - mock({ - '/modules/': mock.load(path.resolve('test/__modules__/'), {}), + mockMemFs({ + '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { adaptors: ['times-two@1.0.0=/modules/times-two'], @@ -439,8 +439,8 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from a relative path in the repo', async (t) => { - mock({ - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), + mockMemFs({ + '/repo/': loadPath(path.resolve('test/__repo__/')), }); const opts = { adaptors: ['times-two'], diff --git a/packages/cli/test/compile/handler.test.ts b/packages/cli/test/compile/handler.test.ts index 24ec95d2e..f7dac7a17 100644 --- a/packages/cli/test/compile/handler.test.ts +++ b/packages/cli/test/compile/handler.test.ts @@ -1,10 +1,10 @@ import test from 'ava'; +import { mockFs, resetMockFs } from '../util'; import fs from 'node:fs/promises'; import { createMockLogger } from '@openfn/logger'; import compileHandler from '../../src/compile/handler'; import { CompileOptions } from '../../src/compile/command'; -import { mockFs, resetMockFs } from '../util'; const logger = createMockLogger(); diff --git a/packages/cli/test/deploy/deploy.test.ts b/packages/cli/test/deploy/deploy.test.ts index ca2356a5d..6998f0c7f 100644 --- a/packages/cli/test/deploy/deploy.test.ts +++ b/packages/cli/test/deploy/deploy.test.ts @@ -1,7 +1,7 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { Logger, createMockLogger } from '@openfn/logger'; import deployHandler, { DeployFn, @@ -17,9 +17,9 @@ const logger = createMockLogger(); const { OPENFN_API_KEY, OPENFN_ENDPOINT, ...originalEnv } = process.env; test.beforeEach(() => { - mockfs.restore(); + restoreFs(); logger._reset(); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, }); @@ -96,7 +96,7 @@ test.serial( 'redirects to beta handler when openfn.yaml exists in cwd', async (t) => { t.plan(3); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -112,7 +112,7 @@ test.serial( test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { t.plan(1); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://api.example.com"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -129,7 +129,7 @@ test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { t.plan(1); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -148,7 +148,7 @@ test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { test.serial( 'openfn.yaml endpoint preferred over config.json endpoint', async (t) => { - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', @@ -164,7 +164,7 @@ test.serial( 'checkout file endpoint preferred over config.json endpoint', async (t) => { t.plan(1); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -179,7 +179,7 @@ test.serial( test.serial('loads the checkout file for the current branch', async (t) => { t.plan(1); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -200,7 +200,7 @@ test.serial('loads the checkout file for the current branch', async (t) => { test.serial('warns if no endpoint can be found when redirecting', async (t) => { t.plan(1); - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, ['./openfn.yaml']: '', @@ -212,7 +212,7 @@ test.serial('warns if no endpoint can be found when redirecting', async (t) => { }); test.serial('CLI apiKey preferred over config.json apiKey', async (t) => { - mockfs({ + mockFs({ ['./config.json']: `{"apiKey": "from-config"}`, ['./project.yaml']: `{"apiKey": "from-config"}`, ['./openfn.yaml']: 'project:\n endpoint: https://from-yaml.org', diff --git a/packages/cli/test/docgen/handler.test.ts b/packages/cli/test/docgen/handler.test.ts index 22f11d17c..6c30dc8ab 100644 --- a/packages/cli/test/docgen/handler.test.ts +++ b/packages/cli/test/docgen/handler.test.ts @@ -1,9 +1,9 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { readFileSync, existsSync } from 'node:fs'; import fs from 'node:fs/promises'; -import mockfs from 'mock-fs'; import { createMockLogger } from '@openfn/logger'; import docsHandler, { DocGenFn, ensurePath } from '../../src/docgen/handler'; @@ -13,9 +13,9 @@ const REPO_PATH = '/tmp/repo'; const DOCS_PATH = `${REPO_PATH}/docs`; test.beforeEach(() => { - mockfs.restore(); + restoreFs(); logger._reset(); - mockfs({ + mockFs({ [DOCS_PATH]: {}, }); }); @@ -79,7 +79,7 @@ test.serial('log the result path', async (t) => { }); test.serial("ensurePath if there's no repo", (t) => { - mockfs({ + mockFs({ ['/tmp']: {}, }); ensurePath('/tmp/repo/docs/x.json'); @@ -88,7 +88,7 @@ test.serial("ensurePath if there's no repo", (t) => { }); test.serial("ensurePath if there's no docs folder", (t) => { - mockfs({ + mockFs({ ['/tmp/repo']: {}, }); ensurePath('/tmp/repo/docs/x.json'); @@ -97,7 +97,7 @@ test.serial("ensurePath if there's no docs folder", (t) => { }); test.serial("ensurePath if there's a namespace", (t) => { - mockfs({ + mockFs({ ['/tmp']: {}, }); ensurePath('/tmp/repo/docs/@openfn/language-common.json'); @@ -114,7 +114,7 @@ test.serial.skip('do not generate docs if they already exist', async (t) => { return {}; }) as unknown as DocGenFn; - mockfs({ + mockFs({ [`${DOCS_PATH}/${specifier}.json`]: '{ "name": "test" }', }); @@ -209,7 +209,7 @@ test.serial.skip("remove the placeholder if there's an error", async (t) => { test.serial.skip('wait for docs if a placeholder is present', async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -242,7 +242,7 @@ test.serial.skip('wait for docs if a placeholder is present', async (t) => { test.serial.skip("throw there's a timeout", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -262,7 +262,7 @@ test.serial.skip( async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -286,7 +286,7 @@ test.serial.skip( test.serial.skip("reset the placeholder if it's old", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; - mockfs({ + mockFs({ [path]: `{ "loading": true, "test", true, "timestamp": ${ Date.now() - 1001 } }`, diff --git a/packages/cli/test/env.test.ts b/packages/cli/test/env.test.ts index 33523f612..802c9f5fd 100644 --- a/packages/cli/test/env.test.ts +++ b/packages/cli/test/env.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from './helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import load, { report } from '../src/env'; @@ -8,11 +8,11 @@ const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { logger._reset(); - mock.restore(); + restoreFs(); }); test('should load and return values from .env', (t) => { - mock({ + mockFs({ '/.env': ` FOO: BAR `, @@ -25,7 +25,7 @@ test('should load and return values from .env', (t) => { }); test('should report values loaded from .env', (t) => { - mock({ + mockFs({ '/.env': ` FOO: BAR `, @@ -46,14 +46,14 @@ test('should report values loaded from .env', (t) => { }); test('should return null if no env is found', (t) => { - mock({}); + mockFs({}); const env = load('/.env'); t.is(env, null); }); test('should log if no env is found', (t) => { - mock({}); + mockFs({}); load('/.env'); @@ -64,7 +64,7 @@ test('should log if no env is found', (t) => { }); test('should use $DOT_ENV_OVERRIDES found', (t) => { - mock({}); + mockFs({}); // don't load .env // but set this magic env var instead diff --git a/packages/cli/test/execute/execute.test.ts b/packages/cli/test/execute/execute.test.ts index 12dac44c1..a519906f9 100644 --- a/packages/cli/test/execute/execute.test.ts +++ b/packages/cli/test/execute/execute.test.ts @@ -3,10 +3,10 @@ // I don't want any io or adaptor tests here, really just looking for the actual execute flow import { createMockLogger } from '@openfn/logger'; import test from 'ava'; +import { mockFs, resetMockFs } from '../util'; import fs from 'node:fs/promises'; import { ExecuteOptions } from '../../src/execute/command'; import handler from '../../src/execute/handler'; -import { mockFs, resetMockFs } from '../util'; const logger = createMockLogger(undefined, { level: 'none' }); diff --git a/packages/cli/test/execute/serialize-output.test.ts b/packages/cli/test/execute/serialize-output.test.ts index a698281d0..3c0cea1af 100644 --- a/packages/cli/test/execute/serialize-output.test.ts +++ b/packages/cli/test/execute/serialize-output.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs } from '../helpers/memfs'; import { readFile } from 'node:fs/promises'; import serializeOutput from '../../src/execute/serialize-output'; @@ -118,7 +118,7 @@ test('exclude configuration', async (t) => { }); test('output to file', async (t) => { - mockfs({ + mockFs({ 'out.json': '', }); diff --git a/packages/cli/test/helpers/memfs.ts b/packages/cli/test/helpers/memfs.ts new file mode 100644 index 000000000..5f822da12 --- /dev/null +++ b/packages/cli/test/helpers/memfs.ts @@ -0,0 +1,119 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so import this before the code under test. + */ + +type FsObject = Record; + +// Tests run from the package folder, two below the repo root +const repoModules = path.resolve('../../node_modules') + path.sep; + +let mem: FsObject | undefined; +const realFds = new Set(); + +// The repo's own node_modules stay on the real disk, so that Node's module +// loader (which reads through node:fs) can still load dependencies lazily +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return realFds.has(target); + if (typeof target === 'string') { + return path.resolve(target).startsWith(repoModules); + } + if (target instanceof URL) { + return path.resolve(fileURLToPath(target)).startsWith(repoModules); + } + return false; +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined +) => { + const real = target[key]; + const fn = (...args: any[]) => { + if (!useReal(args[0])) return getMem()![key](...args); + if (key === 'openSync') { + const fd = real(...args); + realFds.add(fd); + return fd; + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return real(...args, (err: Error | null, fd: number) => { + if (!err) realFds.add(fd); + cb(err, fd); + }); + } + if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); + return real(...args); + }; + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + restoreFs(); + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 9bd5eb277..512aa2e09 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -1,12 +1,12 @@ import test from 'ava'; +import { mockFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { handler as checkoutHandler } from '../../src/projects/checkout'; -import mock from 'mock-fs'; import fs from 'fs'; import { jsonToYaml, Workspace, yamlToJson } from '@openfn/project'; test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, @@ -401,7 +401,7 @@ test.serial.skip('checkout: switch with alias and domain', async (t) => { }); test.serial('respect openfn.yaml settings', async (t) => { - mock({ + mockFs({ '/ws1/w': {}, '/ws1/openfn.yaml': jsonToYaml({ dirs: { workflows: 'w', projects: 'p' }, @@ -548,7 +548,7 @@ uuid: test.serial( 'checkout: removes old workflow directory when workflow is renamed on server', async (t) => { - mock({ + mockFs({ '/ws3/workflows/old-workflow': { 'old-workflow.yaml': jsonToYaml({ id: 'old-workflow', @@ -591,7 +591,7 @@ test.serial( test.serial( 'checkout: removes old step file when step is renamed on server', async (t) => { - mock({ + mockFs({ '/ws4/workflows/my-workflow': { 'my-workflow.yaml': jsonToYaml({ id: 'my-workflow', @@ -633,7 +633,7 @@ test.serial( ); test.serial('checkout: creates credentials.yaml', async (t) => { - mock({ + mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', '/ws2/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -684,7 +684,7 @@ test.serial('checkout: creates credentials.yaml', async (t) => { }); test.serial('checkout: do not create credentials.yaml', async (t) => { - mock({ + mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', '/ws2/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -733,7 +733,7 @@ test.serial('checkout: do not create credentials.yaml', async (t) => { test.serial( 'checkout: removes workflow directory when workflow is deleted on server', async (t) => { - mock({ + mockFs({ '/ws5/workflows/workflow-a': { 'workflow-a.yaml': jsonToYaml({ id: 'workflow-a', @@ -779,7 +779,7 @@ test.serial( test.serial( 'checkout: writes fetched collections into resources.yaml', async (t) => { - mock({ + mockFs({ '/ws6/workflows': {}, '/ws6/openfn.yaml': '', '/ws6/.openfn/checkout.yaml': jsonToYaml({ id: 'main-project' }), @@ -951,7 +951,7 @@ workflows: test.serial( 'Checkout unrelated bar from unrelated project foo without divergence warning', async (t) => { - mock({ + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, @@ -999,7 +999,7 @@ test.serial( test.serial( 'Checkout unrelated foo from unrelated project bar without divergence warning', async (t) => { - mock({ + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, @@ -1047,7 +1047,7 @@ test.serial( test.serial( 'If the checked out project has diverged from the tracked version, show a divergence warning on checkout', async (t) => { - mock({ + mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, '/tmp/.projects/staging@server.yaml': bar, diff --git a/packages/cli/test/projects/clean.test.ts b/packages/cli/test/projects/clean.test.ts index a94926b39..a526bd1d2 100644 --- a/packages/cli/test/projects/clean.test.ts +++ b/packages/cli/test/projects/clean.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { handler as cleanHandler } from '../../src/projects/clean'; -import mock from 'mock-fs'; import fs from 'fs'; import { jsonToYaml } from '@openfn/project'; @@ -38,7 +38,7 @@ const projectStateFile = jsonToYaml({ }); test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)', }, @@ -51,7 +51,7 @@ test.beforeEach(() => { }); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); @@ -94,7 +94,7 @@ test.serial('clean: aborts if user declines confirmation', async (t) => { test.serial( 'clean: throws if no active project found in workspace', async (t) => { - mock({ '/ws/workflows': {} }); + mockFs({ '/ws/workflows': {} }); await t.throwsAsync( () => @@ -111,7 +111,7 @@ test.serial( 'does not delete anything if it is not known which project is checked out', async (t) => { // two tracked projects, but no checkout metadata anywhere - mock({ + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', '/ws/.projects/main@app.openfn.org.yaml': projectStateFile, @@ -133,7 +133,7 @@ test.serial( 'does not delete anything if there is no active project to restore', async (t) => { // a workspace with no projects and no checkout metadata at all - mock({ + mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', }); diff --git a/packages/cli/test/projects/credentials-helpers.test.ts b/packages/cli/test/projects/credentials-helpers.test.ts index 42feb0c02..86e1103f3 100644 --- a/packages/cli/test/projects/credentials-helpers.test.ts +++ b/packages/cli/test/projects/credentials-helpers.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; import fs from 'node:fs'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import Project, { yamlToJson } from '@openfn/project'; @@ -20,7 +20,7 @@ import { test.afterEach(() => { try { - mock.restore(); + restoreFs(); } catch {} }); @@ -151,7 +151,7 @@ test('byCredentialsFile renames a credential per its alias entry', (t) => { }); test('loadCredentialsMapFromFile: an entry with no alias just marks inclusion', (t) => { - mock({ + mockFs({ '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, @@ -162,7 +162,7 @@ test('loadCredentialsMapFromFile: an entry with no alias just marks inclusion', }); test('loadCredentialsMapFromFile: an entry with an alias carries it through', (t) => { - mock({ + mockFs({ '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser alias: other@openfn.org|renamed @@ -176,7 +176,7 @@ test('loadCredentialsMapFromFile: an entry with an alias carries it through', (t }); test('getCredentialsVisitor: a credentials file path syncs only what it lists', (t) => { - mock({ + mockFs({ '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, @@ -213,7 +213,7 @@ test('getCredentialsVisitor: throws when the --credentials map names a credentia }); test('getCredentialsVisitor: throws when a credentials.yaml file names a credential not in the project', (t) => { - mock({ + mockFs({ '/ws/credentials.yaml': `joe@openfn.org|not-a-real-credential: user: someuser `, @@ -333,7 +333,7 @@ test('findCredentialIds: ignores duplicate references', (t) => { }); test('createProjectCredentials: creates credential yaml file', (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); const project = new Project( { @@ -353,7 +353,7 @@ test('createProjectCredentials: creates credential yaml file', (t) => { }); test('createProjectCredentials: preserves existing credentials and adds missing ones', (t) => { - mock({ + mockFs({ '/ws': {}, '/ws/credentials.yaml': `existing: password: secret diff --git a/packages/cli/test/projects/deploy.test.ts b/packages/cli/test/projects/deploy.test.ts index 6abddc2a2..3d3eb9607 100644 --- a/packages/cli/test/projects/deploy.test.ts +++ b/packages/cli/test/projects/deploy.test.ts @@ -1,8 +1,6 @@ import { writeFile } from 'node:fs/promises'; import test from 'ava'; -import mock from 'mock-fs'; -import path from 'node:path'; -import { createRequire } from 'node:module'; +import { mockFs, restoreFs } from '../helpers/memfs'; import fs from 'node:fs'; import Project, { generateWorkflow, @@ -44,28 +42,6 @@ const STRICT_ENDPOINT = `http://localhost:${strictPort}`; const projectYaml = myProject_yaml.replace('https://app.openfn.org', ENDPOINT); const two_workflows_yaml = twowfs.replace('https://app.openfn.org', ENDPOINT); -const require = createRequire(import.meta.url); - -const mockFs = (paths: Record) => { - // ensure this path is available to pnpm (needed by deps for some reason??) - // Note: loading all of pnpm takes ~7 seconds per test - // this workaround cuts out that delay entirely - const iconv = path.resolve( - '../../node_modules/.pnpm/iconv-lite@0.4.24/node_modules/iconv-lite/encodings' - ); - // undici v8 reads its llhttp WASM from disk on the first request, so keep - // that dir visible or fetches to the mock server fail with ENOENT - const undicihttp = path.join( - path.dirname(require.resolve('undici')), - 'lib/llhttp' - ); - mock({ - [iconv]: mock.load(iconv, {}), - [undicihttp]: mock.load(undicihttp, {}), - ...paths, - }); -}; - // Take a project yaml and expand it // This uses checkout to do the heavy lifting const setup = async (yaml: string = projectYaml) => { @@ -92,7 +68,7 @@ test.beforeEach(() => { server.reset(); server.addProject(myProject_v1); logger._reset(); - mock.restore(); + restoreFs(); }); test.serial( @@ -1444,7 +1420,6 @@ test.serial( // simulate a fresh git clone: the workflows are there, nothing is checked // out, and there's more than one tracked project - // (fs.rmSync is a no-op under mock-fs, hence rimraf) await rimraf('/ws/.openfn'); t.false(fs.existsSync('/ws/.openfn')); fs.writeFileSync('/ws/.projects/other@localhost.yaml', projectYaml); diff --git a/packages/cli/test/projects/ensure-checkout.test.ts b/packages/cli/test/projects/ensure-checkout.test.ts index e9ead3df6..37b121278 100644 --- a/packages/cli/test/projects/ensure-checkout.test.ts +++ b/packages/cli/test/projects/ensure-checkout.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; +import { mockFs, restoreFs } from '../helpers/memfs'; import fs from 'node:fs'; -import mock from 'mock-fs'; import { createMockLogger } from '@openfn/logger'; import Project, { Workspace, yamlToJson } from '@openfn/project'; @@ -12,7 +12,7 @@ import ensureCheckout, { const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); @@ -25,7 +25,7 @@ const projectFile = (id: string) => }).serialize('project', { format: 'yaml' }) as string; const setup = (ids = ['main', 'staging'], extra = {}) => - mock({ + mockFs({ '/ws/openfn.yaml': '', '/ws/workflows/wf/wf.yaml': 'id: wf', ...Object.fromEntries( diff --git a/packages/cli/test/projects/fetch.test.ts b/packages/cli/test/projects/fetch.test.ts index b65f7b2de..35659ea3a 100644 --- a/packages/cli/test/projects/fetch.test.ts +++ b/packages/cli/test/projects/fetch.test.ts @@ -1,6 +1,6 @@ import { readFile, writeFile } from 'node:fs/promises'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; @@ -39,7 +39,7 @@ test.before(() => { }); test.beforeEach(() => { - mock({ + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', }); @@ -47,7 +47,7 @@ test.beforeEach(() => { }); test.afterEach(() => { - mock.restore(); + restoreFs(); }); test.serial('fetch by UUID to default new alias', async (t) => { @@ -483,7 +483,7 @@ test.serial( .replace(' - a', ' - z'); // change the local history to be incompatible // Make it look like we've checked out the project - mock({ + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', '/ws/.projects/project@app.openfn.org.yaml': modified, @@ -515,7 +515,7 @@ test.serial('force merge a diverged project', async (t) => { // Change project.yaml const modified = myProject_yaml.replace('fn()', 'fn(x)'); - mock({ + mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', '/ws/.projects/project@app.openfn.org.yaml': modified, @@ -544,7 +544,7 @@ test.serial('force merge a diverged project', async (t) => { test.serial( 'fetch by UUID to an output path when local copies share the UUID', async (t) => { - mock({ + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -570,7 +570,7 @@ test.serial( test.serial( 'error: fetch by UUID to an output path without an endpoint when local copies share the UUID', async (t) => { - mock({ + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -596,7 +596,7 @@ test.serial( test.serial( 'error: fetch by UUID without an output path when local copies share the UUID', async (t) => { - mock({ + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', @@ -620,7 +620,7 @@ test.serial( ); test.serial('fetch by alias when local copies share the UUID', async (t) => { - mock({ + mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, '/ws/openfn.yaml': '', diff --git a/packages/cli/test/projects/list.test.ts b/packages/cli/test/projects/list.test.ts index 2a23e4a90..10fe029bb 100644 --- a/packages/cli/test/projects/list.test.ts +++ b/packages/cli/test/projects/list.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; +import { mockFs } from '../helpers/memfs'; import { handler as list } from '../../src/projects/list'; import { createMockLogger } from '@openfn/logger'; -import mock from 'mock-fs'; import { jsonToYaml } from '@openfn/project'; // a workspace with several projects but no checkout metadata @@ -136,7 +136,7 @@ files['/aliased/workflows/sw/simple-workflow.yaml'] = jsonToYaml({ steps: [], }); -mock(files); +mockFs(files); const logger = createMockLogger('', { level: 'debug' }); diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index 94aed583a..97adce0cd 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -1,6 +1,6 @@ import Project, { jsonToYaml, Workspace } from '@openfn/project'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs } from '../helpers/memfs'; import { handler as mergeHandler } from '../../src/projects/merge'; import { createMockLogger } from '@openfn/logger'; @@ -78,7 +78,7 @@ const main = { }; test.beforeEach(() => { - mock({ + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, @@ -204,7 +204,7 @@ test.serial( ); test.serial('Write to JSON using project config', async (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, formats: { openfn: 'yaml', project: 'json', workflow: 'yaml' }, @@ -247,7 +247,7 @@ test.serial('Write to JSON using project config', async (t) => { }); test.serial('merge with custom base', async (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, formats: { openfn: 'yaml', project: 'yaml', workflow: 'yaml' }, @@ -334,7 +334,7 @@ const multiMain = { }; const mockMultiWorkflowWorkspace = () => { - mock({ + mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, diff --git a/packages/cli/test/projects/util.test.ts b/packages/cli/test/projects/util.test.ts index d9679494f..0c598f75c 100644 --- a/packages/cli/test/projects/util.test.ts +++ b/packages/cli/test/projects/util.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; +import { mockFs, restoreFs } from '../helpers/memfs'; import fs from 'node:fs'; -import mock from 'mock-fs'; import Project, { generateWorkflow, yamlToJson } from '@openfn/project'; import { findLocallyChangedWorkflows, @@ -9,7 +9,7 @@ import { } from '../../src/projects/util'; test.afterEach(() => { - mock.restore(); + restoreFs(); }); test('tidyWorkflowDir: removes workflows that no longer exist', async (t) => { @@ -292,7 +292,7 @@ const trackedProject = () => test.serial( 'writeCheckoutFile: writes the project meta and forked_from to the checkout file', async (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); const filePath = await writeCheckoutFile('/ws', trackedProject()); @@ -311,7 +311,7 @@ test.serial( test.serial( 'writeCheckoutFile: writes to the checkout file for a branch', async (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); const filePath = await writeCheckoutFile('/ws', trackedProject(), 'dev'); @@ -324,7 +324,7 @@ test.serial( test.serial( 'writeCheckoutFile: does not touch the workspace config or workflows', async (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/workflows/wf/wf.yaml': 'id: wf', }); diff --git a/packages/cli/test/pull/handler.test.ts b/packages/cli/test/pull/handler.test.ts index e4761b0b9..95e35c9c0 100644 --- a/packages/cli/test/pull/handler.test.ts +++ b/packages/cli/test/pull/handler.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import fs from 'node:fs'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; @@ -13,13 +13,13 @@ const ENDPOINT = 'https://fake.test.openfn.org'; const PROJECT_UUID = 'e16c5f09-f0cb-4ba7-a4c2-73fcb2f29d00'; test.beforeEach(() => { - mockfs.restore(); + restoreFs(); process.env.OPENFN_API_KEY = 'fake-api-key'; }); test.afterEach(() => { - mockfs.restore(); + restoreFs(); }); let mockAgent = new MockAgent(); @@ -53,7 +53,7 @@ test.serial( 'redirects to beta handler when openfn.yaml exists in cwd', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "${ENDPOINT}"}`, ['/tmp/openfn.yaml']: ` project: @@ -72,7 +72,7 @@ test.serial( 'openfn yaml endpoint preferred to config.json endpoint', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "DOES_NOT_EXIST"}`, ['/tmp/openfn.yaml']: ` project: @@ -90,7 +90,7 @@ test.serial( 'CLI endpoint preferred to openfn.yaml and config.json endpoints', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "DOES_NOT_EXIST"}`, ['/tmp/openfn.yaml']: ` project: @@ -112,7 +112,7 @@ project: test.serial('does not create credentials.yaml when redirecting', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/config.json']: `{"apiKey": "123", "endpoint": "${ENDPOINT}"}`, ['/tmp/openfn.yaml']: ` project: @@ -128,7 +128,7 @@ test.serial( 'tracks the pulled project in the workspace when redirecting with legacy config.yaml structure', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/openfn.yaml']: ` project: endpoint: ${ENDPOINT}`, @@ -150,7 +150,7 @@ test.serial( 'tracks the pulled project in the workspace when redirecting with new checkout.yaml file', async (t) => { const logger = createMockLogger('', { level: 'debug' }); - mockfs({ + mockFs({ ['/tmp/.openfn/checkout.yaml']: ` endpoint: ${ENDPOINT}`, ['/tmp/openfn.yaml']: '', diff --git a/packages/cli/test/util.ts b/packages/cli/test/util.ts index 84312058c..93b2f581a 100644 --- a/packages/cli/test/util.ts +++ b/packages/cli/test/util.ts @@ -1,26 +1,20 @@ /* * test utils */ -import mock from 'mock-fs'; import path from 'node:path'; +import { loadPath, mockFs as mockMemFs, restoreFs } from './helpers/memfs'; import type { ExecutionPlan, Job, StepEdge } from '@openfn/runtime'; export const mockFs = (files: Record) => { - // We have to explicitly expose some modules paths so that dependencies can run in the tests - const pnpm = path.resolve('../../node_modules/.pnpm'); - const recastPath = `${pnpm}/recast@0.21.5`; - const sourceMapPath = `${pnpm}/source-map@0.7.6`; - mock({ - [recastPath]: mock.load(recastPath, {}), - [sourceMapPath]: mock.load(sourceMapPath, {}), - '/repo/': mock.load(path.resolve('test/__repo__/'), {}), + mockMemFs({ + '/repo/': loadPath(path.resolve('test/__repo__/')), ...files, }); }; export const resetMockFs = () => { - mock.restore(); + restoreFs(); }; type CreateWorkflowOptions = { diff --git a/packages/cli/test/util/file-exists.test.ts b/packages/cli/test/util/file-exists.test.ts index a6c8ce7b4..2aab35b43 100644 --- a/packages/cli/test/util/file-exists.test.ts +++ b/packages/cli/test/util/file-exists.test.ts @@ -1,22 +1,22 @@ import test from 'ava'; -import mockfs from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { fileExists } from '../../src/util/file-exists'; test.afterEach(() => { - mockfs.restore(); + restoreFs(); }); test('returns true for an existing file', async (t) => { - mockfs({ './test.txt': 'content' }); + mockFs({ './test.txt': 'content' }); t.true(await fileExists('./test.txt')); }); test('returns false for a non-existent path', async (t) => { - mockfs({}); + mockFs({}); t.false(await fileExists('./nonexistent.txt')); }); test('returns false for a directory', async (t) => { - mockfs({ './mydir': {} }); + mockFs({ './mydir': {} }); t.false(await fileExists('./mydir')); }); diff --git a/packages/cli/test/util/load-plan.test.ts b/packages/cli/test/util/load-plan.test.ts index bc02812cc..3531676dc 100644 --- a/packages/cli/test/util/load-plan.test.ts +++ b/packages/cli/test/util/load-plan.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { omit } from 'lodash-es'; import type { Job } from '@openfn/runtime'; @@ -27,7 +27,7 @@ const createPlan = (steps: Partial[] = []) => ({ }); test.beforeEach(() => { - mock({ + mockFs({ 'test/job.js': 'x', 'test/collections.js': 'collections.get()', 'test/wf-old.json': JSON.stringify({ @@ -42,7 +42,7 @@ test.beforeEach(() => { test.afterEach(() => { logger._reset(); - mock.restore(); + restoreFs(); }); // TODO: add some tests for handling yaml stuff @@ -205,7 +205,7 @@ test.serial('xplan: expand adaptors', async (t) => { }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -235,7 +235,7 @@ test.serial('flat xplan: expand adaptors', async (t) => { }, ], }; - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -263,7 +263,7 @@ test.serial('xplan: do not expand adaptors', async (t) => { }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -293,7 +293,7 @@ test.serial('xplan: set timeout from CLI', async (t) => { // @ts-ignore plan.options.timeout = 1; - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -318,7 +318,7 @@ test.serial('xplan: set start from CLI', async (t) => { // @ts-ignore plan.options.start = 'a'; - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -342,7 +342,7 @@ test.serial('xplan: map to monorepo', async (t) => { }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), '/repo/packages/common': {}, }); @@ -388,7 +388,7 @@ test.serial('step: allow file paths for state', async (t) => { }, ]); - mock({ + mockFs({ 'test/state.json': JSON.stringify({ data: { x: 1, @@ -422,7 +422,7 @@ test.serial('xplan: support multiple adaptors', async (t) => { }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -454,7 +454,7 @@ test.serial('xplan: append collections', async (t) => { }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -496,7 +496,7 @@ test.serial( }, ]); - mock({ + mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -520,7 +520,7 @@ test.serial( test.serial( 'xplan: load a workflow.yaml without top workflow key', async (t) => { - mock({ + mockFs({ 'test/wf.yaml': ` start: a steps: @@ -547,7 +547,7 @@ steps: test.serial( 'xplan: load a workflow.yaml without top workflow key and options', async (t) => { - mock({ + mockFs({ 'test/wf.yaml': ` name: wf steps: @@ -574,7 +574,7 @@ options: ); test.serial('xplan: load a workflow.yaml with top workflow key', async (t) => { - mock({ + mockFs({ 'test/wf.yaml': ` workflow: name: wf @@ -597,7 +597,7 @@ options: }); test.serial('xplan: load a workflow through a Workspace', async (t) => { - mock({ + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: @@ -629,7 +629,7 @@ dirs: }); test.serial('xplan: throw if a named workflow does not exist', async (t) => { - mock({ + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: @@ -655,7 +655,7 @@ dirs: test.serial( 'xplan: load a workflow through a project .yaml and apply the credentials map by default', async (t) => { - mock({ + mockFs({ '/tmp/workflows/wf.yaml': ` id: wf steps: diff --git a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts index 83218a7a6..d3a50992e 100644 --- a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts +++ b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import mapAdaptorsToMonorepo, { @@ -18,11 +18,11 @@ const logger = createMockLogger(); test.afterEach(() => { logger._reset(); - mock.restore(); + restoreFs(); }); test.serial('updatePath: common', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -32,7 +32,7 @@ test.serial('updatePath: common', (t) => { }); test.serial('updatePath: @openfn/language-common', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -42,7 +42,7 @@ test.serial('updatePath: @openfn/language-common', (t) => { }); test.serial('updatePath: common@1.2.3 (with warning)', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -62,7 +62,7 @@ test('updatePath: common=x/y/z', (t) => { }); test.serial('updatePath: prefer the root which has the adaptor', (t) => { - mock({ + mockFs({ [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -73,7 +73,7 @@ test.serial('updatePath: prefer the root which has the adaptor', (t) => { }); test.serial('updatePath: earlier root wins when both have the adaptor', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -84,7 +84,7 @@ test.serial('updatePath: earlier root wins when both have the adaptor', (t) => { }); test.serial('updatePath: throw if not found in the single root', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, }); @@ -94,7 +94,7 @@ test.serial('updatePath: throw if not found in the single root', (t) => { }); test.serial('updatePath: throw if not found in any root', (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, [`${REPO_PATH_2}/packages`]: {}, }); @@ -106,7 +106,7 @@ test.serial('updatePath: throw if not found in any root', (t) => { // TODO can't test this in ava, have to use an integration test test.skip('validate monorepo: log and exit early if repo not found', async (t) => { - mock({ + mockFs({ a: {}, }); @@ -119,7 +119,7 @@ test.skip('validate monorepo: log and exit early if repo not found', async (t) = }); test.serial('validate monorepo: all OK', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, }); @@ -127,7 +127,7 @@ test.serial('validate monorepo: all OK', async (t) => { }); test.serial('validate monorepo: all OK with multiple paths', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages`]: {}, [`${REPO_PATH_2}/packages`]: {}, }); @@ -138,7 +138,7 @@ test.serial('validate monorepo: all OK with multiple paths', async (t) => { }); test.serial('mapAdaptorsToMonorepo: map adaptors', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); @@ -149,7 +149,7 @@ test.serial('mapAdaptorsToMonorepo: map adaptors', async (t) => { test.serial( 'mapAdaptorsToMonorepo: map adaptors across multiple roots', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/http`]: {}, [`${REPO_PATH_2}/packages/common`]: {}, }); @@ -167,7 +167,7 @@ test.serial( ); test.serial('mapAdaptorsToMonorepo: map workflow', async (t) => { - mock({ + mockFs({ [`${REPO_PATH}/packages/common`]: {}, }); diff --git a/packages/cli/test/util/print-versions.test.ts b/packages/cli/test/util/print-versions.test.ts index 0ad35dd55..841621cac 100644 --- a/packages/cli/test/util/print-versions.test.ts +++ b/packages/cli/test/util/print-versions.test.ts @@ -1,6 +1,6 @@ import { createMockLogger, JSONLog } from '@openfn/logger'; import test from 'ava'; -import mock from 'mock-fs'; +import { loadPath, mockFs } from '../helpers/memfs'; import path from 'node:path'; import printVersions from '../../src/util/print-versions'; @@ -85,9 +85,9 @@ test('print versions for node, cli and long-form adaptor with version', async (t }); test('print version of adaptor with monorepo', async (t) => { - mock({ + mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -103,9 +103,9 @@ test('print version of adaptor with monorepo', async (t) => { }); test('print version of adaptor with path', async (t) => { - mock({ + mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -120,9 +120,9 @@ test('print version of adaptor with path', async (t) => { }); test('print version of adaptor with path even if monorepo is set', async (t) => { - mock({ + mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); @@ -138,9 +138,9 @@ test('print version of adaptor with path even if monorepo is set', async (t) => }); test('print version of adaptor with path and @', async (t) => { - mock({ + mockFs({ '/repo/node_modules/@openfn/http/package.json': '{ "version": "1.0.0" }', - [root]: mock.load(root, {}), + [root]: loadPath(root), }); const logger = createMockLogger('', { level: 'info' }); diff --git a/packages/cli/test/util/validate-adaptors.test.ts b/packages/cli/test/util/validate-adaptors.test.ts index 2e7a209c2..acfc4bb61 100644 --- a/packages/cli/test/util/validate-adaptors.test.ts +++ b/packages/cli/test/util/validate-adaptors.test.ts @@ -1,13 +1,13 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import mockfs from 'mock-fs'; +import { restoreFs } from '../helpers/memfs'; import validateAdaptors from '../../src/util/validate-adaptors'; const logger = createMockLogger('', { level: 'default' }); test.afterEach(() => { logger._reset(); - mockfs.restore(); + restoreFs(); }); test.serial('should warn if expression passed with no adaptor', async (t) => { diff --git a/packages/deploy/package.json b/packages/deploy/package.json index 58b8230b0..9a392c640 100644 --- a/packages/deploy/package.json +++ b/packages/deploy/package.json @@ -31,11 +31,10 @@ "@inquirer/testing": "^2.1.53", "@types/json-diff": "^1.0.3", "@types/jsonpath": "^0.2.4", - "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.130", "ava": "5.3.1", "jsonpath": "^1.3.0", - "mock-fs": "^5.5.0", + "memfs": "^4.80.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/deploy/test/helpers/memfs.ts b/packages/deploy/test/helpers/memfs.ts new file mode 100644 index 000000000..5f822da12 --- /dev/null +++ b/packages/deploy/test/helpers/memfs.ts @@ -0,0 +1,119 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so import this before the code under test. + */ + +type FsObject = Record; + +// Tests run from the package folder, two below the repo root +const repoModules = path.resolve('../../node_modules') + path.sep; + +let mem: FsObject | undefined; +const realFds = new Set(); + +// The repo's own node_modules stay on the real disk, so that Node's module +// loader (which reads through node:fs) can still load dependencies lazily +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return realFds.has(target); + if (typeof target === 'string') { + return path.resolve(target).startsWith(repoModules); + } + if (target instanceof URL) { + return path.resolve(fileURLToPath(target)).startsWith(repoModules); + } + return false; +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined +) => { + const real = target[key]; + const fn = (...args: any[]) => { + if (!useReal(args[0])) return getMem()![key](...args); + if (key === 'openSync') { + const fd = real(...args); + realFds.add(fd); + return fd; + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return real(...args, (err: Error | null, fd: number) => { + if (!err) realFds.add(fd); + cb(err, fd); + }); + } + if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); + return real(...args); + }; + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + restoreFs(); + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/packages/deploy/test/validator.test.ts b/packages/deploy/test/validator.test.ts index d963db1b0..03ee5843c 100644 --- a/packages/deploy/test/validator.test.ts +++ b/packages/deploy/test/validator.test.ts @@ -1,5 +1,5 @@ -import mock from 'mock-fs'; import test from 'ava'; +import { mockFs, restoreFs } from './helpers/memfs'; import { parseAndValidate } from '../src/validator'; function findError(errors: any[], message: string) { @@ -7,7 +7,7 @@ function findError(errors: any[], message: string) { } test.after(() => { - mock.restore(); + restoreFs(); }); test('Workflows must be a map', async (t) => { @@ -173,7 +173,7 @@ test('parses channels with destination_credential', async (t) => { test('adds the file content into the job body from the specified path', async (t) => { // Step 1: Create a temporary file that the YAML will reference const fileContent = 'fn(state => state.data);'; - mock({ + mockFs({ '/jobBody.js': fileContent, }); diff --git a/packages/project/package.json b/packages/project/package.json index f0ddc3158..a799cb28e 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -28,9 +28,8 @@ "keywords": [], "devDependencies": { "@types/lodash": "^4.17.24", - "@types/mock-fs": "~4.13.4", "ava": "5.3.1", - "mock-fs": "^5.5.0", + "memfs": "^4.80.0", "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsup": "^8.5.1", diff --git a/packages/project/test/helpers/memfs.ts b/packages/project/test/helpers/memfs.ts new file mode 100644 index 000000000..5f822da12 --- /dev/null +++ b/packages/project/test/helpers/memfs.ts @@ -0,0 +1,119 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so import this before the code under test. + */ + +type FsObject = Record; + +// Tests run from the package folder, two below the repo root +const repoModules = path.resolve('../../node_modules') + path.sep; + +let mem: FsObject | undefined; +const realFds = new Set(); + +// The repo's own node_modules stay on the real disk, so that Node's module +// loader (which reads through node:fs) can still load dependencies lazily +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return realFds.has(target); + if (typeof target === 'string') { + return path.resolve(target).startsWith(repoModules); + } + if (target instanceof URL) { + return path.resolve(fileURLToPath(target)).startsWith(repoModules); + } + return false; +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined +) => { + const real = target[key]; + const fn = (...args: any[]) => { + if (!useReal(args[0])) return getMem()![key](...args); + if (key === 'openSync') { + const fd = real(...args); + realFds.add(fd); + return fd; + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return real(...args, (err: Error | null, fd: number) => { + if (!err) realFds.add(fd); + cb(err, fd); + }); + } + if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); + return real(...args); + }; + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + restoreFs(); + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index 22dc5d4a2..950f30a07 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -1,12 +1,12 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { parseProject } from '../../src/parse/from-fs'; import { jsonToYaml, yamlToJson } from '../../src/util/yaml'; import { buildConfig } from '../../src/util/config'; test.afterEach(() => { files = {}; - mock.restore(); + restoreFs(); }); let files: Record = {}; @@ -20,7 +20,7 @@ function mockFile(path: string, content: string | object) { // @ts-ignore files[path] = content; - mock(files); + mockFs(files); } test.serial('should include multiple workflows (legacy format)', async (t) => { diff --git a/packages/project/test/parse/from-path.test.ts b/packages/project/test/parse/from-path.test.ts index 088b14d75..f291d8d63 100644 --- a/packages/project/test/parse/from-path.test.ts +++ b/packages/project/test/parse/from-path.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs } from '../helpers/memfs'; import { generateProject } from '../../src'; import fromPath, { extractAliasFromFilename } from '../../src/parse/from-path'; @@ -10,7 +10,7 @@ const proj = generateProject('my-project', ['a-b'], { }); test.serial('should load a v1 state json', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.json': JSON.stringify( proj.serialize('state', { format: 'json' }) ), @@ -26,7 +26,7 @@ test.serial('should load a v1 state json', async (t) => { }); test.serial('should load a v1 state yaml', async (t) => { - mock({ + mockFs({ // @ts-ignore '/p1/main@openfn.org.yaml': proj.serialize('state', { format: 'yaml' }), }); @@ -41,7 +41,7 @@ test.serial('should load a v1 state yaml', async (t) => { }); test.serial('should load a v2 project yaml', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.yaml': v2.yaml, }); const project = await fromPath('/p1/main@openfn.org.yaml'); @@ -52,7 +52,7 @@ test.serial('should load a v2 project yaml', async (t) => { }); test.serial('should load a v2 project json', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.json': JSON.stringify(v2.json), }); const project = await fromPath('/p1/main@openfn.org.json'); @@ -63,7 +63,7 @@ test.serial('should load a v2 project json', async (t) => { }); test.serial('should use workspace config', async (t) => { - mock({ + mockFs({ // @ts-ignore '/p1/main@openfn.org.yaml': proj.serialize('state', { format: 'yaml' }), }); @@ -95,7 +95,7 @@ test.serial('should use workspace config', async (t) => { }); test.serial('should apply a name override to a loaded project', async (t) => { - mock({ + mockFs({ '/p1/main@openfn.org.yaml': v2.yaml, }); const project = await fromPath('/p1/main@openfn.org.yaml', { diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 0cd128af3..5ce334f46 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -1,4 +1,5 @@ import test from 'ava'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { extractCheckout, extractConfig, @@ -7,11 +8,10 @@ import { findWorkspaceFile, loadWorkspaceFile, } from '../../src/util/config'; -import mock from 'mock-fs'; import Project from '../../src'; test.afterEach(() => { - mock.restore(); + restoreFs(); }); test('load config as yaml', (t) => { @@ -146,7 +146,7 @@ test('legacy: load empty config', (t) => { }); test('find openfn.yaml', (t) => { - mock({ '/tmp/openfn.yaml': 'x: 1' }); + mockFs({ '/tmp/openfn.yaml': 'x: 1' }); const result = findWorkspaceFile('/tmp'); t.is(result.type, 'yaml'); @@ -154,7 +154,7 @@ test('find openfn.yaml', (t) => { }); test('find openfn.json', (t) => { - mock({ '/tmp/openfn.json': '{ "x": 1 }' }); + mockFs({ '/tmp/openfn.json': '{ "x": 1 }' }); const result = findWorkspaceFile('/tmp'); t.is(result.type, 'json'); @@ -281,19 +281,19 @@ test('include project name', (t) => { test.todo('include parent project name'); test('hasCheckoutMeta: false if there is no workspace at all', (t) => { - mock({ '/ws': {} }); + mockFs({ '/ws': {} }); t.false(hasCheckoutMeta('/ws')); }); test('hasCheckoutMeta: false if there is a workspace but no checkout file', (t) => { - mock({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); t.false(hasCheckoutMeta('/ws')); }); test('hasCheckoutMeta: true if there is a checkout file', (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': 'id: my-project', }); @@ -302,7 +302,7 @@ test('hasCheckoutMeta: true if there is a checkout file', (t) => { }); test('hasCheckoutMeta: false if the checkout file is empty', (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': '', }); @@ -311,7 +311,7 @@ test('hasCheckoutMeta: false if the checkout file is empty', (t) => { }); test('hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'project:\n id: my-project\nworkspace:\n credentials: creds.yaml', }); @@ -320,7 +320,7 @@ test('hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', }); test('hasCheckoutMeta: looks for the checkout file for the given branch', (t) => { - mock({ + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/branches/dev/checkout.yaml': 'id: my-project', }); diff --git a/packages/project/test/workspace.test.ts b/packages/project/test/workspace.test.ts index b8ae20410..8e918777b 100644 --- a/packages/project/test/workspace.test.ts +++ b/packages/project/test/workspace.test.ts @@ -1,6 +1,6 @@ -import mock from 'mock-fs'; import { jsonToYaml, Workspace } from '../src'; import test from 'ava'; +import { mockFs } from './helpers/memfs'; const gen = (uuid: any, _alias: string, id: string, _domain: string) => jsonToYaml({ @@ -14,7 +14,7 @@ const gen = (uuid: any, _alias: string, id: string, _domain: string) => }); // TODO need a test on the legacy and new yaml formats here -mock({ +mockFs({ '/ws/openfn.yaml': jsonToYaml({ project: { id: 'project-1', diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 2cc920459..571c0a1ba 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -32,11 +32,10 @@ "@openfn/compiler": "workspace:^", "@openfn/language-common": "3.2.3", "@openfn/lexicon": "workspace:^", - "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.130", "@types/semver": "^7.7.1", "ava": "5.3.1", - "mock-fs": "^5.5.0", + "memfs": "^4.80.0", "recast": "^0.21.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", diff --git a/packages/runtime/test/helpers/memfs.ts b/packages/runtime/test/helpers/memfs.ts new file mode 100644 index 000000000..5f822da12 --- /dev/null +++ b/packages/runtime/test/helpers/memfs.ts @@ -0,0 +1,119 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so import this before the code under test. + */ + +type FsObject = Record; + +// Tests run from the package folder, two below the repo root +const repoModules = path.resolve('../../node_modules') + path.sep; + +let mem: FsObject | undefined; +const realFds = new Set(); + +// The repo's own node_modules stay on the real disk, so that Node's module +// loader (which reads through node:fs) can still load dependencies lazily +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return realFds.has(target); + if (typeof target === 'string') { + return path.resolve(target).startsWith(repoModules); + } + if (target instanceof URL) { + return path.resolve(fileURLToPath(target)).startsWith(repoModules); + } + return false; +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined +) => { + const real = target[key]; + const fn = (...args: any[]) => { + if (!useReal(args[0])) return getMem()![key](...args); + if (key === 'openSync') { + const fd = real(...args); + realFds.add(fd); + return fd; + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return real(...args, (err: Error | null, fd: number) => { + if (!err) realFds.add(fd); + cb(err, fd); + }); + } + if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); + return real(...args); + }; + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + restoreFs(); + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/packages/runtime/test/repo/ensure-repo.test.ts b/packages/runtime/test/repo/ensure-repo.test.ts index 49e7247b6..060e1ec3d 100644 --- a/packages/runtime/test/repo/ensure-repo.test.ts +++ b/packages/runtime/test/repo/ensure-repo.test.ts @@ -1,19 +1,19 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { ensureRepo, loadRepoPkg } from '../../src/modules/repo'; const logger = createMockLogger('test', { level: 'debug' }); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); const defaultPkg = JSON.stringify({ name: 'test' }); test.serial('return the existing package json', async (t) => { - mock({ + mockFs({ '/tmp/repo/package.json': defaultPkg, }); const result = await ensureRepo('/tmp/repo', logger); @@ -22,7 +22,7 @@ test.serial('return the existing package json', async (t) => { }); test.serial('do not modify the existing package.json', async (t) => { - mock({ + mockFs({ '/tmp/repo/package.json': defaultPkg, }); const result = await ensureRepo('/tmp/repo', logger); @@ -36,7 +36,7 @@ test.serial('do not modify the existing package.json', async (t) => { test.serial( 'create and return a package.json if the repo does not exist', async (t) => { - mock({}); + mockFs({}); const result = await ensureRepo('/tmp/repo', logger); t.truthy(result); t.is(result.name, 'openfn-repo'); @@ -53,7 +53,7 @@ test.serial( test.serial( 'create and return a package if the repo does not exist at a long path', async (t) => { - mock({}); + mockFs({}); const result = await ensureRepo('/a/b/c/d/repo', logger); t.truthy(result); t.is(result.name, 'openfn-repo'); @@ -64,7 +64,7 @@ test.serial( ); test.serial('log if a repo is created', async (t) => { - mock({}); + mockFs({}); await ensureRepo('/tmp/repo', logger); const { message } = logger._parse(logger._last); t.regex(message as string, /Creating new repo/); diff --git a/packages/runtime/test/repo/get-module-entry-point.test.ts b/packages/runtime/test/repo/get-module-entry-point.test.ts index cd941cbc2..4f7aeb69b 100644 --- a/packages/runtime/test/repo/get-module-entry-point.test.ts +++ b/packages/runtime/test/repo/get-module-entry-point.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import path from 'node:path'; import { createMockLogger } from '@openfn/logger'; import { getModuleEntryPoint } from '../../src/modules/repo'; @@ -7,14 +7,14 @@ import { getModuleEntryPoint } from '../../src/modules/repo'; const logger = createMockLogger(); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); const repoPath = path.resolve('test/__repo__'); const mockRepo = (pkg: object) => { - mock({ + mockFs({ // Mock a repo with one module installed [`${repoPath}/package.json`]: JSON.stringify({ name: 'test', diff --git a/packages/runtime/test/repo/get-module-path.test.ts b/packages/runtime/test/repo/get-module-path.test.ts index 924ab2024..4ec37f976 100644 --- a/packages/runtime/test/repo/get-module-path.test.ts +++ b/packages/runtime/test/repo/get-module-path.test.ts @@ -1,19 +1,19 @@ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { getModulePath } from '../../src/modules/repo'; const logger = createMockLogger(); test.afterEach(() => { - mock.restore(); + restoreFs(); logger._reset(); }); const repoPath = '/tmp/repo/'; const mockPkg = (dependencies: object) => { - mock({ + mockFs({ [`${repoPath}/package.json`]: JSON.stringify({ name: 'test', private: true, diff --git a/packages/runtime/test/repo/install.test.ts b/packages/runtime/test/repo/install.test.ts index a2f276f8c..d6af38278 100644 --- a/packages/runtime/test/repo/install.test.ts +++ b/packages/runtime/test/repo/install.test.ts @@ -3,7 +3,7 @@ * Testing with actual installs is left as an exercise for integration testing */ import test from 'ava'; -import mock from 'mock-fs'; +import { mockFs, restoreFs } from '../helpers/memfs'; import { createMockLogger } from '@openfn/logger'; import { install, loadRepoPkg } from '../../src'; import exec from '../../src/util/exec'; @@ -11,12 +11,12 @@ import exec from '../../src/util/exec'; const mockLogger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - mock.restore(); + restoreFs(); mockLogger._reset(); }); test.beforeEach(() => { - mock({ + mockFs({ '/tmp': {}, }); }); @@ -139,7 +139,7 @@ test.serial('install with the correct alias', async (t) => { test.serial('do nothing if the package is already installed', async (t) => { const exec = mockExec(); - mock({ + mockFs({ '/tmp/repo/package.json': JSON.stringify({ name: 'test', dependencies: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a741c902..1d38b9691 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,10 +174,10 @@ importers: version: 2.30.0(@types/node@25.6.0) '@slack/web-api': specifier: ^7.16.0 - version: 7.16.0 + version: 7.16.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) '@swc-node/register': specifier: ^1.11.1 - version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3) + version: 1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@7.2.0)(typescript@5.9.3) '@types/gunzip-maybe': specifier: ^1.4.3 version: 1.4.3 @@ -219,7 +219,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -256,7 +256,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -281,7 +281,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -318,13 +318,13 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) date-fns: specifier: ^2.30.0 version: 2.30.0 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@7.2.0) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -343,7 +343,7 @@ importers: dependencies: ava: specifier: ^5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) packages/cli: dependencies: @@ -417,9 +417,6 @@ importers: '@types/lodash-es': specifier: ~4.17.12 version: 4.17.12 - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 '@types/node': specifier: ^18.19.130 version: 18.19.130 @@ -437,19 +434,19 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) lodash-es: specifier: ^4.17.23 version: 4.17.23 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + memfs: + specifier: ^4.80.0 + version: 4.80.0 tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -483,13 +480,13 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -524,27 +521,24 @@ importers: '@types/jsonpath': specifier: ^0.2.4 version: 0.2.4 - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 '@types/node': specifier: ^18.19.130 version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) jsonpath: specifier: ^1.3.0 version: 1.3.0 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + memfs: + specifier: ^4.80.0 + version: 4.80.0 tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -553,7 +547,7 @@ importers: dependencies: '@typescript/vfs': specifier: ^1.6.4 - version: 1.6.4(typescript@5.9.3) + version: 1.6.4(supports-color@7.2.0)(typescript@5.9.3) cross-fetch: specifier: ^3.2.0 version: 3.2.0(encoding@0.1.13) @@ -578,7 +572,7 @@ importers: version: 3.0.2 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) esbuild: specifier: ^0.18.20 version: 0.18.20 @@ -587,7 +581,7 @@ importers: version: 6.1.3 threads: specifier: 1.7.0 - version: 1.7.0 + version: 1.7.0(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -596,7 +590,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) packages/engine-multi: dependencies: @@ -624,7 +618,7 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -633,7 +627,7 @@ importers: version: 2.3.0 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -655,10 +649,10 @@ importers: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@7.2.0)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@7.2.0))(supports-color@7.2.0) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -691,7 +685,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@7.2.0) koa-logger: specifier: ^4.0.0 version: 4.0.0 @@ -725,10 +719,10 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) koa-websocket: specifier: ^7.0.0 - version: 7.0.0 + version: 7.0.0(supports-color@7.2.0) query-string: specifier: ^8.2.0 version: 8.2.0 @@ -737,7 +731,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -765,13 +759,13 @@ importers: version: 18.19.130 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@7.2.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -809,15 +803,12 @@ importers: '@types/lodash': specifier: ^4.17.24 version: 4.17.24 - '@types/mock-fs': - specifier: ~4.13.4 - version: 4.13.4 ava: specifier: 5.3.1 - version: 5.3.1 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + version: 5.3.1(supports-color@7.2.0) + memfs: + specifier: ^4.80.0 + version: 4.80.0 rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -826,7 +817,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -858,9 +849,6 @@ importers: '@openfn/lexicon': specifier: workspace:^ version: link:../lexicon - '@types/mock-fs': - specifier: ^4.13.4 - version: 4.13.4 '@types/node': specifier: ^18.19.130 version: 18.19.130 @@ -869,10 +857,10 @@ importers: version: 7.7.1 ava: specifier: 5.3.1 - version: 5.3.1 - mock-fs: - specifier: ^5.5.0 - version: 5.5.0 + version: 5.3.1(supports-color@7.2.0) + memfs: + specifier: ^4.80.0 + version: 4.80.0 recast: specifier: ^0.21.5 version: 0.21.5 @@ -884,7 +872,7 @@ importers: version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -893,10 +881,10 @@ importers: dependencies: '@koa/bodyparser': specifier: ^6.1.0 - version: 6.1.0(koa@2.16.4) + version: 6.1.0(koa@2.16.4(supports-color@5.5.0)) '@koa/router': specifier: ^15.4.0 - version: 15.4.0(koa@2.16.4) + version: 15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0) '@openfn/engine-multi': specifier: workspace:* version: link:../engine-multi @@ -911,7 +899,7 @@ importers: version: link:../runtime '@sentry/node': specifier: ^10.44.0 - version: 10.44.0 + version: 10.44.0(supports-color@5.5.0) fast-safe-stringify: specifier: ^2.1.1 version: 2.1.1 @@ -926,7 +914,7 @@ importers: version: 4.15.9 koa: specifier: ^2.16.4 - version: 2.16.4 + version: 2.16.4(supports-color@5.5.0) koa-logger: specifier: ^3.2.1 version: 3.2.1 @@ -966,19 +954,19 @@ importers: version: 17.0.35 ava: specifier: 5.3.1 - version: 5.3.1 + version: 5.3.1(supports-color@5.5.0) nodemon: specifier: 3.1.14 version: 3.1.14 sentry-testkit: specifier: ^6.2.2 - version: 6.2.2 + version: 6.2.2(supports-color@5.5.0) tslib: specifier: ^2.8.1 version: 2.8.1 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) + version: 8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2) typescript: specifier: ^4.9.5 version: 4.9.5 @@ -1486,6 +1474,126 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/base64@17.67.0': + resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@17.67.0': + resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@17.67.0': + resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-core@4.80.0': + resolution: {integrity: sha512-qMKWshnyjbyhm+NbzqGE3w5y1mckFFEMj0aDPU2/JBNh1BzIjVy9esq9lBmRnNVmhMEkxwDzLZ8Gqh5G1P5Dag==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-fsa@4.80.0': + resolution: {integrity: sha512-ZBEKl7J6dbkRCfmrFV48Re211A/FHqniTUhsWCsaRteNSOqMgkX3qF1UreVgwB9oSPm81JLAwhIi6Lb/NH/PoA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-builtins@4.80.0': + resolution: {integrity: sha512-OIOIhqaWiwUySFMny4epIAoviXsyMaQQ63PTc3zH+5HEOvarl5hpUmZIhp2m5+OfyKAh5ylxXnQIgew1OgJDVw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-to-fsa@4.80.0': + resolution: {integrity: sha512-ljeaR5xwtX1EyRmB0wvXANUMQZ3mjKcm7z8s2N8D8SLQ04CcAGtUhHFRZQXFb9mhgFyEVMydQGQtSOFS7pcrFQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-utils@4.80.0': + resolution: {integrity: sha512-xwHeVVi3f+Khg/Fmb8i5qoYVmQ08QznE5TWhQokUmIZjz4KBPzU+zt/+bahBI6G1ghtqajt27lWuAciIxApuzA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node@4.80.0': + resolution: {integrity: sha512-yiBlUfkMMRFFGy8CD/h2zd5rPnrM6bjlF+nx2LRAxNV6hOSiuObQIUEE7m7y2VTMisjxCRtzINH94bFGySWL7g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-print@4.80.0': + resolution: {integrity: sha512-1ocZLJw/m/xlq0moaIHO7HV9nHRVBUvdc2zMzhsJGkFOrqfwWvexfCv5cYCPPLRhP+9p3yunoi4Ep5YhRORC9Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-snapshot@4.80.0': + resolution: {integrity: sha512-3AV23Q1H228KTtJKTUE0nNq8FCtxSvtENOLrEat80wkAJCKHuqQEv8yjhpS8Bei2OEFJAnluGXp9Ri2SWBlROw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@17.67.0': + resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@17.67.0': + resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@17.67.0': + resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@koa/bodyparser@6.1.0': resolution: {integrity: sha512-thVG/Utbz9+dB4Nl8EBJKoaOI1DzO74dJqeDFILbAVhUq6C+4rmmrKFwxiDE63ScywOs0CHypx1IUrSRMueFTw==} engines: {node: '>= 18'} @@ -2232,9 +2340,6 @@ packages: '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - '@types/mock-fs@4.13.4': - resolution: {integrity: sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==} - '@types/mute-stream@0.0.1': resolution: {integrity: sha512-0yQLzYhCqGz7CQPE3iDmYjhb7KMBFOP+tBkyw+/Y2YyDI5wpS7itXXxneN1zSsUwWx3Ji6YiVYrhAnpQGS/vkw==} @@ -3235,6 +3340,12 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regex.js@1.3.1: + resolution: {integrity: sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -3310,6 +3421,10 @@ packages: humanize-number@0.0.2: resolution: {integrity: sha512-un3ZAcNQGI7RzaWGZzQDH47HETM4Wrj6z6E4TId8Yeq9w5ZKUVB1nrT2jwFheTUjEmqcgTjXDc959jum+ai1kQ==} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3601,6 +3716,9 @@ packages: resolution: {integrity: sha512-F2t4YIv9XQUBHt6AOJ0y7lSmP1+cY7Fm1DRh9GClTGzKST7UWLMx6ly9WZdLH/G/ppM5RL4MlQfRT71ri9t19A==} engines: {node: '>=12.20'} + memfs@4.80.0: + resolution: {integrity: sha512-N70W2isi44XdGn7uPo9NJ4zRukcdkwSFHlnYx42SWOH/+hzqNc8P7+tVoA2up4z7faHvNi1RSNCFjxoYQ71VPQ==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -3652,10 +3770,6 @@ packages: mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - mock-fs@5.5.0: - resolution: {integrity: sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==} - engines: {node: '>=12.0.0'} - module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} @@ -4270,6 +4384,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@2.6.1: + resolution: {integrity: sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + threads@1.7.0: resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} @@ -4309,6 +4429,12 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -4860,11 +4986,11 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@fastify/otel@0.17.1(@opentelemetry/api@1.9.0)': + '@fastify/otel@0.17.1(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 minimatch: 10.2.4 transitivePeerDependencies: @@ -5041,19 +5167,165 @@ snapshots: dependencies: jsep: 1.4.0 - '@koa/bodyparser@6.1.0(koa@2.16.4)': + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-core@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-fsa@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-builtins@4.80.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-to-fsa@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-utils@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-print@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-snapshot@4.80.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@5.5.0))': + dependencies: + '@types/co-body': 6.1.3 + co-body: 6.2.0 + koa: 2.16.4(supports-color@5.5.0) + lodash.merge: 4.6.2 + type-is: 2.0.1 + + '@koa/bodyparser@6.1.0(koa@2.16.4(supports-color@7.2.0))': dependencies: '@types/co-body': 6.1.3 co-body: 6.2.0 - koa: 2.16.4 + koa: 2.16.4(supports-color@7.2.0) lodash.merge: 4.6.2 type-is: 2.0.1 - '@koa/router@15.4.0(koa@2.16.4)': + '@koa/router@15.4.0(koa@2.16.4(supports-color@5.5.0))(supports-color@5.5.0)': dependencies: debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 - koa: 2.16.4 + koa: 2.16.4(supports-color@5.5.0) + koa-compose: 4.1.0 + path-to-regexp: 8.4.1 + transitivePeerDependencies: + - supports-color + + '@koa/router@15.4.0(koa@2.16.4(supports-color@7.2.0))(supports-color@7.2.0)': + dependencies: + debug: 4.4.3(supports-color@7.2.0) + http-errors: 2.0.1 + koa: 2.16.4(supports-color@7.2.0) koa-compose: 4.1.0 path-to-regexp: 8.4.1 transitivePeerDependencies: @@ -5179,163 +5451,163 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/instrumentation-amqplib@0.60.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-amqplib@0.60.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-connect@0.56.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-connect@0.56.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-dataloader@0.30.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-dataloader@0.30.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-express@0.61.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-express@0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fs@0.32.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-fs@0.32.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-generic-pool@0.56.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-generic-pool@0.56.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.61.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-graphql@0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-hapi@0.59.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-hapi@0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-http@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.61.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-kafkajs@0.22.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-kafkajs@0.22.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-knex@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-knex@0.57.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-koa@0.61.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-koa@0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-lru-memoizer@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-lru-memoizer@0.57.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongodb@0.66.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongodb@0.66.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.59.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mongoose@0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql2@0.59.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql2@0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql@0.59.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-mysql@0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 '@types/mysql': 2.15.27 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.65.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.65.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 @@ -5343,57 +5615,57 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-redis@0.61.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-redis@0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-tedious@0.32.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-tedious@0.32.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-undici@0.23.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-undici@0.23.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.207.0 import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.212.0 import-in-the-middle: 2.0.6 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.213.0 import-in-the-middle: 3.0.0 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -5481,10 +5753,10 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)': + '@prisma/instrumentation@7.4.2(@opentelemetry/api@1.9.0)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -5565,7 +5837,7 @@ snapshots: '@sentry/core@10.44.0': {} - '@sentry/node-core@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': + '@sentry/node-core@10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0)': dependencies: '@sentry/core': 10.44.0 '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) @@ -5574,46 +5846,46 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 - '@sentry/node@10.44.0': + '@sentry/node@10.44.0(supports-color@5.5.0)': dependencies: - '@fastify/otel': 0.17.1(@opentelemetry/api@1.9.0) + '@fastify/otel': 0.17.1(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.60.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.30.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.56.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.213.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.22.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.57.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.66.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.59.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.65.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis': 0.61.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.32.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.23.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-amqplib': 0.60.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-connect': 0.56.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-dataloader': 0.30.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-express': 0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-fs': 0.32.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-generic-pool': 0.56.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-graphql': 0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-hapi': 0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-http': 0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-ioredis': 0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-kafkajs': 0.22.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-knex': 0.57.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-koa': 0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.57.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-mongodb': 0.66.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-mongoose': 0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-mysql': 0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-mysql2': 0.59.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-pg': 0.65.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-redis': 0.61.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-tedious': 0.32.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) + '@opentelemetry/instrumentation-undici': 0.23.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.40.0 - '@prisma/instrumentation': 7.4.2(@opentelemetry/api@1.9.0) + '@prisma/instrumentation': 7.4.2(@opentelemetry/api@1.9.0)(supports-color@5.5.0) '@sentry/core': 10.44.0 - '@sentry/node-core': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/node-core': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.213.0(@opentelemetry/api@1.9.0)(supports-color@5.5.0))(@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) '@sentry/opentelemetry': 10.44.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.40.0) import-in-the-middle: 3.0.0 transitivePeerDependencies: @@ -5634,13 +5906,13 @@ snapshots: '@slack/types@2.21.1': {} - '@slack/web-api@7.16.0': + '@slack/web-api@7.16.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@slack/logger': 4.0.1 '@slack/types': 2.21.1 '@types/node': 25.6.0 '@types/retry': 0.12.0 - axios: 1.16.1 + axios: 1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) eventemitter3: 5.0.4 form-data: 4.0.5 is-electron: 2.2.2 @@ -5657,13 +5929,13 @@ snapshots: '@swc/core': 1.15.18 '@swc/types': 0.1.25 - '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(typescript@5.9.3)': + '@swc-node/register@1.11.1(@swc/core@1.15.18)(@swc/types@0.1.25)(supports-color@7.2.0)(typescript@5.9.3)': dependencies: '@swc-node/core': 1.14.1(@swc/core@1.15.18)(@swc/types@0.1.25) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.18 colorette: 2.0.20 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) oxc-resolver: 11.19.1 pirates: 4.0.7 tslib: 2.8.1 @@ -5837,10 +6109,6 @@ snapshots: '@types/lodash@4.17.24': {} - '@types/mock-fs@4.13.4': - dependencies: - '@types/node': 18.19.130 - '@types/mute-stream@0.0.1': dependencies: '@types/node': 18.19.130 @@ -5937,9 +6205,9 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript/vfs@1.6.4(typescript@5.9.3)': + '@typescript/vfs@1.6.4(supports-color@7.2.0)(typescript@5.9.3)': dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5959,9 +6227,9 @@ snapshots: acorn@8.16.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@7.2.0): dependencies: - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -6039,7 +6307,7 @@ snapshots: asynckit@0.4.0: {} - ava@5.3.1: + ava@5.3.1(supports-color@5.5.0): dependencies: acorn: 8.16.0 acorn-walk: 8.3.5 @@ -6087,11 +6355,59 @@ snapshots: transitivePeerDependencies: - supports-color - axios@1.16.1: + ava@5.3.1(supports-color@7.2.0): dependencies: - follow-redirects: 1.16.0 + acorn: 8.16.0 + acorn-walk: 8.3.5 + ansi-styles: 6.2.3 + arrgv: 1.0.2 + arrify: 3.0.0 + callsites: 4.2.0 + cbor: 8.1.0 + chalk: 5.6.2 + chokidar: 3.6.0 + chunkd: 2.0.1 + ci-info: 3.9.0 + ci-parallel-vars: 1.0.1 + clean-yaml-object: 0.1.0 + cli-truncate: 3.1.0 + code-excerpt: 4.0.0 + common-path-prefix: 3.0.0 + concordance: 5.0.4 + currently-unhandled: 0.4.1 + debug: 4.4.3(supports-color@7.2.0) + emittery: 1.2.1 + figures: 5.0.0 + globby: 13.2.2 + ignore-by-default: 2.1.0 + indent-string: 5.0.0 + is-error: 2.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + matcher: 5.0.0 + mem: 9.0.2 + ms: 2.1.3 + p-event: 5.0.1 + p-map: 5.5.0 + picomatch: 2.3.1 + pkg-conf: 4.0.0 + plur: 5.1.0 + pretty-ms: 8.0.0 + resolve-cwd: 3.0.0 + stack-utils: 2.0.6 + strip-ansi: 7.2.0 + supertap: 3.0.1 + temp-dir: 3.0.0 + write-file-atomic: 5.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + axios@1.16.1(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.5 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@7.2.0) proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -6142,11 +6458,11 @@ snapshots: blueimp-md5@2.19.0: {} - body-parser@1.20.4: + body-parser@1.20.4(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -6404,9 +6720,11 @@ snapshots: dependencies: time-zone: 1.0.0 - debug@2.6.9: + debug@2.6.9(supports-color@5.5.0): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 5.5.0 debug@4.4.3(supports-color@5.5.0): dependencies: @@ -6414,6 +6732,12 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.4.3(supports-color@7.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 + decode-uri-component@0.4.1: {} deep-equal@1.0.1: {} @@ -6706,21 +7030,21 @@ snapshots: transitivePeerDependencies: - bare-abort-controller - express@4.22.1: + express@4.22.1(supports-color@5.5.0): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.4(supports-color@5.5.0) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@5.5.0) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -6732,8 +7056,8 @@ snapshots: qs: 6.14.2 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@5.5.0) + serve-static: 1.16.3(supports-color@5.5.0) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -6793,9 +7117,9 @@ snapshots: filter-obj@5.1.0: {} - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -6821,7 +7145,9 @@ snapshots: mlly: 1.8.1 rollup: 4.59.0 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) form-data@4.0.5: dependencies: @@ -6884,6 +7210,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.3.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob@13.0.6: dependencies: minimatch: 10.2.4 @@ -6966,10 +7296,10 @@ snapshots: http-status-codes@2.3.0: {} - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@7.2.0): dependencies: - agent-base: 6.0.2 - debug: 4.4.3(supports-color@5.5.0) + agent-base: 6.0.2(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -6977,6 +7307,8 @@ snapshots: humanize-number@0.0.2: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -7155,10 +7487,10 @@ snapshots: on-finished: 2.4.1 passthrough-counter: 1.0.0 - koa-websocket@7.0.0: + koa-websocket@7.0.0(supports-color@7.2.0): dependencies: co: 4.6.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) koa-compose: 4.1.0 ws: 8.19.0 transitivePeerDependencies: @@ -7166,7 +7498,7 @@ snapshots: - supports-color - utf-8-validate - koa@2.16.4: + koa@2.16.4(supports-color@5.5.0): dependencies: accepts: 1.3.8 cache-content-type: 1.0.1 @@ -7194,6 +7526,34 @@ snapshots: transitivePeerDependencies: - supports-color + koa@2.16.4(supports-color@7.2.0): + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + debug: 4.4.3(supports-color@7.2.0) + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.1.2 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -7253,6 +7613,23 @@ snapshots: map-age-cleaner: 0.1.3 mimic-fn: 4.0.0 + memfs@4.80.0: + dependencies: + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + merge-descriptors@1.0.3: {} merge2@1.4.1: {} @@ -7293,8 +7670,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - mock-fs@5.5.0: {} - module-details-from-path@1.0.4: {} mri@1.2.0: {} @@ -7646,7 +8021,7 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: + require-in-the-middle@8.0.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) module-details-from-path: 1.0.4 @@ -7721,9 +8096,9 @@ snapshots: semver@7.7.4: {} - send@0.19.2: + send@0.19.2(supports-color@5.5.0): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@5.5.0) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -7739,10 +8114,10 @@ snapshots: transitivePeerDependencies: - supports-color - sentry-testkit@6.2.2: + sentry-testkit@6.2.2(supports-color@5.5.0): dependencies: - body-parser: 1.20.4 - express: 4.22.1 + body-parser: 1.20.4(supports-color@5.5.0) + express: 4.22.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7750,12 +8125,12 @@ snapshots: dependencies: type-fest: 0.13.1 - serve-static@1.16.3: + serve-static@1.16.3(supports-color@5.5.0): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7947,10 +8322,14 @@ snapshots: dependencies: any-promise: 1.3.0 - threads@1.7.0: + thingies@2.6.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + + threads@1.7.0(supports-color@7.2.0): dependencies: callsites: 3.1.0 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -7991,6 +8370,10 @@ snapshots: tr46@0.0.3: {} + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} treeify@1.1.0: {} @@ -8025,7 +8408,7 @@ snapshots: tsscmp@1.0.6: {} - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@5.5.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -8054,13 +8437,42 @@ snapshots: - tsx - yaml - tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@4.9.5)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3(supports-color@7.2.0) + esbuild: 0.27.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2) + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.15.18 + postcss: 8.5.8 + typescript: 4.9.5 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsup@8.5.1(@swc/core@1.15.18)(jiti@1.21.7)(postcss@8.5.8)(supports-color@7.2.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3(supports-color@7.2.0) esbuild: 0.27.4 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 From 57994b0cc1bc4c71b23673d30d6cdf043f9fe45d Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 18:23:59 +0200 Subject: [PATCH 09/12] Add failing tests for the memfs helper's known gaps --- packages/cli/test/memfs.test.ts | 69 ++++++++++++++++++++++++ packages/cli/test/projects/merge.test.ts | 12 +++++ packages/project/test/workspace.test.ts | 17 +++++- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/cli/test/memfs.test.ts diff --git a/packages/cli/test/memfs.test.ts b/packages/cli/test/memfs.test.ts new file mode 100644 index 000000000..3fe4d351a --- /dev/null +++ b/packages/cli/test/memfs.test.ts @@ -0,0 +1,69 @@ +import test from 'ava'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { createRequire } from 'node:module'; +import { mockFs, restoreFs } from './helpers/memfs'; + +const require = createRequire(import.meta.url); + +// Resolved without require, so that require's own caches stay cold +const realModule = fs.realpathSync('node_modules/treeify/treeify.js'); + +test.afterEach(restoreFs); + +test.serial('require loads a package dependency while mocked', (t) => { + mockFs(); + const treeify = require('treeify'); + t.is(typeof treeify.asTree, 'function'); +}); + +test.serial('copyFileSync from node_modules writes into memfs', (t) => { + const dest = path.join(os.tmpdir(), `memfs-copy-${process.pid}.js`); + mockFs(); + try { + fs.copyFileSync(realModule, dest); + t.true(fs.existsSync(dest)); + restoreFs(); + t.false(fs.existsSync(dest)); + } finally { + restoreFs(); + fs.rmSync(dest, { force: true }); + } +}); + +test.serial('fstatSync works on stdout while mocked', (t) => { + mockFs(); + t.notThrows(() => fs.fstatSync(1)); +}); + +test.serial('fds from fs.promises.open can be read and closed', async (t) => { + mockFs({ '/a.txt': 'hello' }); + for (const [file, expected] of [ + ['/a.txt', 'hello'], + [realModule, fs.readFileSync(realModule, 'utf8').slice(0, 5)], + ]) { + const handle = await fs.promises.open(file); + const buffer = Buffer.alloc(5); + fs.readSync(handle.fd, buffer, 0, 5, 0); + t.is(buffer.toString(), expected); + await handle.close(); + } +}); + +test.serial('promisified exists resolves true for a mocked path', async (t) => { + mockFs({ '/a.txt': 'hello' }); + t.true(await promisify(fs.exists)('/a.txt')); +}); + +test.serial('a Buffer path into node_modules reads the real disk', (t) => { + mockFs(); + const content = fs.readFileSync(Buffer.from(realModule), 'utf8'); + t.true(content.length > 0); +}); + +test.serial('mockFs throws while a mock is already active', (t) => { + mockFs(); + t.throws(() => mockFs()); +}); diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index 97adce0cd..b5f5a691b 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -3,6 +3,7 @@ import test from 'ava'; import { mockFs } from '../helpers/memfs'; import { handler as mergeHandler } from '../../src/projects/merge'; import { createMockLogger } from '@openfn/logger'; +import { handler as checkoutHandler } from '../../src/projects/checkout'; const sandbox = { id: '', @@ -95,6 +96,17 @@ test.beforeEach(() => { const logger = createMockLogger('', { level: 'debug' }); +test.serial('checkout finds the project workflows on disk', async (t) => { + await checkoutHandler( + { command: 'project-checkout', project: 'my-sandbox', workspace: '/ws' }, + logger + ); + + const project = await Project.from('fs', { root: '/ws' }); + t.is(project.workflows.length, 1); + t.is(project.workflows[0].steps.length, 3); +}); + test.serial('merging into the same project', async (t) => { await mergeHandler( { diff --git a/packages/project/test/workspace.test.ts b/packages/project/test/workspace.test.ts index 8e918777b..cee1696c9 100644 --- a/packages/project/test/workspace.test.ts +++ b/packages/project/test/workspace.test.ts @@ -1,4 +1,4 @@ -import { jsonToYaml, Workspace } from '../src'; +import Project, { jsonToYaml, Workspace } from '../src'; import test from 'ava'; import { mockFs } from './helpers/memfs'; @@ -31,6 +31,16 @@ mockFs({ custom: true, }, }), + '/ws/workflows/wf-1/wf-1.yaml': jsonToYaml({ + id: 'wf-1', + steps: [{ id: 'a', expression: 'a.js' }], + }), + '/ws/workflows/wf-1/a.js': 'fn(s => s)', + '/ws/workflows/wf-2/wf-2.yaml': jsonToYaml({ + id: 'wf-2', + steps: [{ id: 'b', expression: 'b.js' }], + }), + '/ws/workflows/wf-2/b.js': 'fn(s => s)', '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ name: 'Project 1', id: '', @@ -240,6 +250,11 @@ mockFs({ '/ws4/.projects/staging@openfn.org.yaml': gen(113, 'staging', 'proj-1-staging', 'openfn.org'), }); +test('load the checked out project from the filesystem', async (t) => { + const project = await Project.from('fs', { root: '/ws' }); + t.is(project.workflows.length, 2); +}); + test('workspace-path: valid workspace path', (t) => { const ws = new Workspace('/ws'); t.is(ws.valid, true); From d7d7f046827888e8eb5bcab2a2408671c32d6f86 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 18:33:10 +0200 Subject: [PATCH 10/12] Share one memfs helper across kit's tests, preloaded by ava --- ava.config.js | 9 + packages/cli/package.json | 2 +- .../cli/test/collections/collections.test.ts | 6 +- packages/cli/test/commands.test.ts | 6 +- packages/cli/test/compile/compile.test.ts | 29 +- packages/cli/test/compile/handler.test.ts | 6 +- packages/cli/test/deploy/deploy.test.ts | 68 ++- packages/cli/test/docgen/handler.test.ts | 14 +- packages/cli/test/env.test.ts | 15 +- packages/cli/test/execute/execute.test.ts | 6 +- .../cli/test/execute/serialize-output.test.ts | 24 +- packages/cli/test/helpers/memfs.ts | 119 ---- packages/cli/test/memfs.test.ts | 9 +- packages/cli/test/projects/checkout.test.ts | 14 +- packages/cli/test/projects/clean.test.ts | 8 +- .../test/projects/credentials-helpers.test.ts | 489 +++++++++-------- packages/cli/test/projects/deploy.test.ts | 432 ++++++++------- .../cli/test/projects/ensure-checkout.test.ts | 33 +- packages/cli/test/projects/fetch.test.ts | 14 +- packages/cli/test/projects/list.test.ts | 14 +- packages/cli/test/projects/merge.test.ts | 7 +- packages/cli/test/projects/util.test.ts | 355 ++++++------ packages/cli/test/pull/handler.test.ts | 10 +- packages/cli/test/util.ts | 14 - packages/cli/test/util/file-exists.test.ts | 12 +- packages/cli/test/util/load-plan.test.ts | 21 +- .../util/map-adaptors-to-monorepo.test.ts | 7 +- packages/cli/test/util/print-versions.test.ts | 128 +++-- .../cli/test/util/validate-adaptors.test.ts | 2 - packages/deploy/package.json | 2 +- packages/deploy/test/helpers/memfs.ts | 119 ---- packages/deploy/test/validator.test.ts | 57 +- packages/project/package.json | 2 +- packages/project/test/helpers/memfs.ts | 119 ---- packages/project/test/parse/from-fs.test.ts | 6 +- packages/project/test/parse/from-path.test.ts | 47 +- packages/project/test/util/config.test.ts | 93 ++-- packages/project/test/workspace.test.ts | 507 +++++++++--------- packages/runtime/package.json | 2 +- packages/runtime/test/helpers/memfs.ts | 119 ---- .../runtime/test/repo/ensure-repo.test.ts | 5 +- .../test/repo/get-module-entry-point.test.ts | 8 +- .../runtime/test/repo/get-module-path.test.ts | 5 +- packages/runtime/test/repo/install.test.ts | 6 +- packages/test-helpers/package.json | 13 + packages/test-helpers/src/memfs.ts | 181 +++++++ pnpm-lock.yaml | 30 +- 47 files changed, 1584 insertions(+), 1610 deletions(-) delete mode 100644 packages/cli/test/helpers/memfs.ts delete mode 100644 packages/deploy/test/helpers/memfs.ts delete mode 100644 packages/project/test/helpers/memfs.ts delete mode 100644 packages/runtime/test/helpers/memfs.ts create mode 100644 packages/test-helpers/package.json create mode 100644 packages/test-helpers/src/memfs.ts diff --git a/ava.config.js b/ava.config.js index df5cb8e81..87934e13e 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,6 +1,14 @@ +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); const semver = require("semver") const loader_arg = semver.lte(process.version, 'v20.5.0') ? '--loader=@swc-node/register/esm' : '--import=@swc-node/register/esm-register' + +// Loaded before any test file, so that every module sees the patched fs +const memfs = pathToFileURL( + path.join(__dirname, 'packages/test-helpers/src/memfs.ts') +).href; + module.exports = { extensions: { ts: 'module', @@ -12,6 +20,7 @@ module.exports = { nodeArguments: [ loader_arg, + `--import=${memfs}`, '--no-warnings', // Disable experimental module warnings '--experimental-vm-modules', ], diff --git a/packages/cli/package.json b/packages/cli/package.json index 76b3a622d..a52983505 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,6 +38,7 @@ "devDependencies": { "@openfn/language-collections": "^0.9.0", "@openfn/language-common": "3.2.3", + "@openfn/test-helpers": "workspace:*", "@types/json-diff": "^1.0.3", "@types/lodash-es": "~4.17.12", "@types/node": "^18.19.130", @@ -47,7 +48,6 @@ "@types/yargs": "^17.0.35", "ava": "5.3.1", "lodash-es": "^4.17.23", - "memfs": "^4.80.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/cli/test/collections/collections.test.ts b/packages/cli/test/collections/collections.test.ts index d761bdd6f..c95ed1668 100644 --- a/packages/cli/test/collections/collections.test.ts +++ b/packages/cli/test/collections/collections.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, resetMockFs } from '../util'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { get, set, remove } from '../../src/collections/handler'; @@ -10,6 +10,8 @@ import { createMockLogger } from '@openfn/logger'; import { collections } from '@openfn/language-collections'; import { readFile } from 'fs/promises'; +test.afterEach(restoreFs); + // Log as json to make testing easier const logger = createMockLogger('default', { level: 'debug', json: true }); @@ -49,8 +51,6 @@ test.beforeEach(() => { x: {}, y: {}, }); - - resetMockFs(); }); const createOptions = (opts = {}) => ({ diff --git a/packages/cli/test/commands.test.ts b/packages/cli/test/commands.test.ts index 34442b193..06b15a4dc 100644 --- a/packages/cli/test/commands.test.ts +++ b/packages/cli/test/commands.test.ts @@ -1,6 +1,6 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import { loadPath, mockFs, restoreFs } from './helpers/memfs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import { execSync } from 'node:child_process'; import fs from 'node:fs/promises'; import os from 'node:os'; @@ -11,6 +11,8 @@ import { cmd } from '../src/cli'; import commandParser from '../src/commands'; import type { Opts } from '../src/options'; +test.afterEach(restoreFs); + // These CLI tests take quite a while to run, mostly I think // because of all the FS mocking // To make them more robust in CI, all the tests in this file need @@ -20,7 +22,6 @@ const TIMEOUT = 1000 * 30; const logger = createMockLogger('', { level: 'debug' }); test.afterEach(() => { - restoreFs(); logger._reset(); }); @@ -69,7 +70,6 @@ async function run(command: string, job: string, options: RunOptions = {}) { '/modules/': loadPath(path.resolve('test/__modules__/')), '/repo/': loadPath(path.resolve('test/__repo__/')), '/monorepo/': loadPath(path.resolve('test/__monorepo__/')), - //'node_modules': loadPath(path.resolve('node_modules/')), [pkgPath]: loadPath(pkgPath), ...(options.mockfs ?? {}), }); diff --git a/packages/cli/test/compile/compile.test.ts b/packages/cli/test/compile/compile.test.ts index 72ea84402..2cb9fa226 100644 --- a/packages/cli/test/compile/compile.test.ts +++ b/packages/cli/test/compile/compile.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { loadPath, mockFs as mockMemFs, restoreFs } from '../helpers/memfs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createMockLogger } from '@openfn/logger'; @@ -11,13 +11,12 @@ import compile, { compileProject, } from '../../src/compile/compile'; import { CompileOptions } from '../../src/compile/command'; -import { mockFs, resetMockFs } from '../util'; import type { ExecutionPlan, Job } from '@openfn/runtime'; -const mockLog = createMockLogger(); +test.afterEach(restoreFs); -test.after(resetMockFs); +const mockLog = createMockLogger(); const expressionPath = '/job.js'; @@ -100,8 +99,6 @@ steps: const code = await fs.readFile(outPaths[0], 'utf-8'); t.true(code.includes('export default [x()]')); - - restoreFs(); }); test.serial( @@ -131,8 +128,6 @@ steps: // Only step-a has exportable code — step-b should not be written t.is(outPaths.length, 1); t.true(outPaths[0].endsWith('step-a.mjs')); - - restoreFs(); } ); @@ -161,8 +156,6 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/proj/build/')); - - restoreFs(); } ); @@ -198,8 +191,6 @@ steps: ); t.is(outPaths.length, 1); await t.throwsAsync(() => fs.access('/proj/dist/old-workflow/stale.mjs')); - - restoreFs(); } ); @@ -225,8 +216,6 @@ steps: t.is(outPaths.length, 1); t.true(outPaths[0].startsWith('/out/')); - - restoreFs(); }); test.serial('throw an AbortError if a job is uncompilable', async (t) => { @@ -314,7 +303,7 @@ test.serial('loadTransformOptions: do nothing', async (t) => { test.serial( "resolveSpecifierPath: return null if the module can't be resolved locally", async (t) => { - mockMemFs({ + mockFs({ '/repo': {}, }); const path = await resolveSpecifierPath('pkg', '/repo', mockLog); @@ -360,7 +349,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return a path to the repo if the module is found', async (t) => { - mockMemFs({ + mockFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -376,7 +365,7 @@ test.serial( test.serial( 'resolveSpecifierPath: return null if a module is not in the repo', async (t) => { - mockMemFs({ + mockFs({ '/repo/package.json': JSON.stringify({ name: 'repo', dependencies: { @@ -392,7 +381,7 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path', async (t) => { - mockMemFs({ + mockFs({ '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { @@ -416,7 +405,7 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from an explicit path and version specifier', async (t) => { - mockMemFs({ + mockFs({ '/modules/': loadPath(path.resolve('test/__modules__/')), }); const opts = { @@ -439,7 +428,7 @@ test.serial( test.serial( 'loadTransformOptions: describes imports from a relative path in the repo', async (t) => { - mockMemFs({ + mockFs({ '/repo/': loadPath(path.resolve('test/__repo__/')), }); const opts = { diff --git a/packages/cli/test/compile/handler.test.ts b/packages/cli/test/compile/handler.test.ts index f7dac7a17..887fcce47 100644 --- a/packages/cli/test/compile/handler.test.ts +++ b/packages/cli/test/compile/handler.test.ts @@ -1,19 +1,19 @@ import test from 'ava'; -import { mockFs, resetMockFs } from '../util'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import { createMockLogger } from '@openfn/logger'; import compileHandler from '../../src/compile/handler'; import { CompileOptions } from '../../src/compile/command'; +test.afterEach(restoreFs); + const logger = createMockLogger(); test.afterEach(() => { logger._reset(); }); -test.after(resetMockFs); - test.serial('compile an expression file to stdout', async (t) => { mockFs({ '/job.js': 'x();', diff --git a/packages/cli/test/deploy/deploy.test.ts b/packages/cli/test/deploy/deploy.test.ts index 6998f0c7f..82231fcf1 100644 --- a/packages/cli/test/deploy/deploy.test.ts +++ b/packages/cli/test/deploy/deploy.test.ts @@ -1,7 +1,7 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { Logger, createMockLogger } from '@openfn/logger'; import deployHandler, { DeployFn, @@ -12,13 +12,15 @@ import { yamlToJson } from '@openfn/project'; import { DeployError, type DeployConfig } from '@openfn/deploy'; import { DeployOptions } from '../../src/deploy/command'; +test.afterEach(restoreFs); + const logger = createMockLogger(); const { OPENFN_API_KEY, OPENFN_ENDPOINT, ...originalEnv } = process.env; test.beforeEach(() => { - restoreFs(); logger._reset(); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -96,6 +98,7 @@ test.serial( 'redirects to beta handler when openfn.yaml exists in cwd', async (t) => { t.plan(3); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -112,6 +115,7 @@ test.serial( test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { t.plan(1); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://api.example.com"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -129,6 +133,7 @@ test.serial('does not redirect when PREFER_LEGACY_SYNC is set', async (t) => { test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { t.plan(1); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -148,6 +153,7 @@ test.serial('CLI endpoint preferred over openfn.yaml endpoint', async (t) => { test.serial( 'openfn.yaml endpoint preferred over config.json endpoint', async (t) => { + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -164,6 +170,7 @@ test.serial( 'checkout file endpoint preferred over config.json endpoint', async (t) => { t.plan(1); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -179,6 +186,7 @@ test.serial( test.serial('loads the checkout file for the current branch', async (t) => { t.plan(1); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123", "endpoint": "https://from-config.org"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -200,6 +208,7 @@ test.serial('loads the checkout file for the current branch', async (t) => { test.serial('warns if no endpoint can be found when redirecting', async (t) => { t.plan(1); + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "123"}`, ['./project.yaml']: `{"apiKey": "123"}`, @@ -212,6 +221,7 @@ test.serial('warns if no endpoint can be found when redirecting', async (t) => { }); test.serial('CLI apiKey preferred over config.json apiKey', async (t) => { + restoreFs(); mockFs({ ['./config.json']: `{"apiKey": "from-config"}`, ['./project.yaml']: `{"apiKey": "from-config"}`, @@ -290,12 +300,12 @@ workflows: configuration: super@openfn.org|http1 `; -test('maybeConvertV2spec: returns v1 yaml unchanged', async (t) => { +test.serial('maybeConvertV2spec: returns v1 yaml unchanged', async (t) => { const result = await maybeConvertV2spec(v1Yaml); t.is(result, v1Yaml); }); -test('maybeConvertV2spec: converts v2 to v1', async (t) => { +test.serial('maybeConvertV2spec: converts v2 to v1', async (t) => { const result = await maybeConvertV2spec(v2Yaml); const json = yamlToJson(result) as any; @@ -319,7 +329,7 @@ test('maybeConvertV2spec: converts v2 to v1', async (t) => { t.falsy(json.schema_version); }); -test('maybeConvertV2spec: converts with credentials', async (t) => { +test.serial('maybeConvertV2spec: converts with credentials', async (t) => { const result = await maybeConvertV2spec(v2Yaml); const json = yamlToJson(result) as any; @@ -333,26 +343,31 @@ test('maybeConvertV2spec: converts with credentials', async (t) => { ); }); -test('maybeConvertV2spec: converted edges use key references, not UUIDs', async (t) => { - const result = await maybeConvertV2spec(v2Yaml); - const json = yamlToJson(result) as any; +test.serial( + 'maybeConvertV2spec: converted edges use key references, not UUIDs', + async (t) => { + const result = await maybeConvertV2spec(v2Yaml); + const json = yamlToJson(result) as any; - const workflow = Object.values(json.workflows)[0] as any; - const edge = Object.values(workflow.edges)[0] as any; + const workflow = Object.values(json.workflows)[0] as any; + const edge = Object.values(workflow.edges)[0] as any; - // edge must use spec format (key references) so mergeSpecIntoState can resolve them - t.truthy(edge.source_trigger); - t.truthy(edge.target_job); - t.falsy(edge.source_trigger_id); - t.falsy(edge.target_job_id); + // edge must use spec format (key references) so mergeSpecIntoState can resolve them + t.truthy(edge.source_trigger); + t.truthy(edge.target_job); + t.falsy(edge.source_trigger_id); + t.falsy(edge.target_job_id); - // source_trigger must match a trigger key; target_job must match a job key - t.truthy(workflow.triggers[edge.source_trigger]); - t.truthy(workflow.jobs[edge.target_job]); -}); + // source_trigger must match a trigger key; target_job must match a job key + t.truthy(workflow.triggers[edge.source_trigger]); + t.truthy(workflow.jobs[edge.target_job]); + } +); -test('maybeConvertV2spec: converts legacy v2 (cli.version: 2) to v1', async (t) => { - const legacyV2Yaml = `id: my-project +test.serial( + 'maybeConvertV2spec: converts legacy v2 (cli.version: 2) to v1', + async (t) => { + const legacyV2Yaml = `id: my-project name: My Project cli: version: 2 @@ -365,9 +380,10 @@ workflows: type: webhook enabled: true `; - const result = await maybeConvertV2spec(legacyV2Yaml); - const json = yamlToJson(result) as any; + const result = await maybeConvertV2spec(legacyV2Yaml); + const json = yamlToJson(result) as any; - t.is(typeof json.workflows, 'object'); - t.false(Array.isArray(json.workflows)); -}); + t.is(typeof json.workflows, 'object'); + t.false(Array.isArray(json.workflows)); + } +); diff --git a/packages/cli/test/docgen/handler.test.ts b/packages/cli/test/docgen/handler.test.ts index 6c30dc8ab..e08fbb761 100644 --- a/packages/cli/test/docgen/handler.test.ts +++ b/packages/cli/test/docgen/handler.test.ts @@ -1,20 +1,22 @@ // Test the actual functionality of docgen // ie, generate docs to a mock folder import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { readFileSync, existsSync } from 'node:fs'; import fs from 'node:fs/promises'; import { createMockLogger } from '@openfn/logger'; import docsHandler, { DocGenFn, ensurePath } from '../../src/docgen/handler'; +test.afterEach(restoreFs); + const logger = createMockLogger(); const REPO_PATH = '/tmp/repo'; const DOCS_PATH = `${REPO_PATH}/docs`; test.beforeEach(() => { - restoreFs(); logger._reset(); + restoreFs(); mockFs({ [DOCS_PATH]: {}, }); @@ -79,6 +81,7 @@ test.serial('log the result path', async (t) => { }); test.serial("ensurePath if there's no repo", (t) => { + restoreFs(); mockFs({ ['/tmp']: {}, }); @@ -88,6 +91,7 @@ test.serial("ensurePath if there's no repo", (t) => { }); test.serial("ensurePath if there's no docs folder", (t) => { + restoreFs(); mockFs({ ['/tmp/repo']: {}, }); @@ -97,6 +101,7 @@ test.serial("ensurePath if there's no docs folder", (t) => { }); test.serial("ensurePath if there's a namespace", (t) => { + restoreFs(); mockFs({ ['/tmp']: {}, }); @@ -114,6 +119,7 @@ test.serial.skip('do not generate docs if they already exist', async (t) => { return {}; }) as unknown as DocGenFn; + restoreFs(); mockFs({ [`${DOCS_PATH}/${specifier}.json`]: '{ "name": "test" }', }); @@ -209,6 +215,7 @@ test.serial.skip("remove the placeholder if there's an error", async (t) => { test.serial.skip('wait for docs if a placeholder is present', async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; + restoreFs(); mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -242,6 +249,7 @@ test.serial.skip('wait for docs if a placeholder is present', async (t) => { test.serial.skip("throw there's a timeout", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; + restoreFs(); mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -262,6 +270,7 @@ test.serial.skip( async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; + restoreFs(); mockFs({ [path]: `{ "loading": true, "timestamp": ${Date.now()} }`, }); @@ -286,6 +295,7 @@ test.serial.skip( test.serial.skip("reset the placeholder if it's old", async (t) => { const path = `${DOCS_PATH}/${specifier}.json`; + restoreFs(); mockFs({ [path]: `{ "loading": true, "test", true, "timestamp": ${ Date.now() - 1001 diff --git a/packages/cli/test/env.test.ts b/packages/cli/test/env.test.ts index 802c9f5fd..7af97e184 100644 --- a/packages/cli/test/env.test.ts +++ b/packages/cli/test/env.test.ts @@ -1,17 +1,18 @@ import test from 'ava'; -import { mockFs, restoreFs } from './helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import load, { report } from '../src/env'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { logger._reset(); - restoreFs(); }); -test('should load and return values from .env', (t) => { +test.serial('should load and return values from .env', (t) => { mockFs({ '/.env': ` FOO: BAR @@ -24,7 +25,7 @@ test('should load and return values from .env', (t) => { }); }); -test('should report values loaded from .env', (t) => { +test.serial('should report values loaded from .env', (t) => { mockFs({ '/.env': ` FOO: BAR @@ -45,14 +46,14 @@ test('should report values loaded from .env', (t) => { t.truthy(c); }); -test('should return null if no env is found', (t) => { +test.serial('should return null if no env is found', (t) => { mockFs({}); const env = load('/.env'); t.is(env, null); }); -test('should log if no env is found', (t) => { +test.serial('should log if no env is found', (t) => { mockFs({}); load('/.env'); @@ -63,7 +64,7 @@ test('should log if no env is found', (t) => { t.truthy(a); }); -test('should use $DOT_ENV_OVERRIDES found', (t) => { +test.serial('should use $DOT_ENV_OVERRIDES found', (t) => { mockFs({}); // don't load .env diff --git a/packages/cli/test/execute/execute.test.ts b/packages/cli/test/execute/execute.test.ts index a519906f9..e480fbe27 100644 --- a/packages/cli/test/execute/execute.test.ts +++ b/packages/cli/test/execute/execute.test.ts @@ -3,11 +3,13 @@ // I don't want any io or adaptor tests here, really just looking for the actual execute flow import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import { mockFs, resetMockFs } from '../util'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs/promises'; import { ExecuteOptions } from '../../src/execute/command'; import handler from '../../src/execute/handler'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'none' }); // These options are designed to minimise i/o @@ -28,8 +30,6 @@ const defaultOptions = { const fn = `const fn = (fn) => (s) => fn(s); `; -test.after(resetMockFs); - test.serial('run a simple job', async (t) => { const job = `${fn}fn(() => ({ data: 42 }));`; diff --git a/packages/cli/test/execute/serialize-output.test.ts b/packages/cli/test/execute/serialize-output.test.ts index 3c0cea1af..442f275a9 100644 --- a/packages/cli/test/execute/serialize-output.test.ts +++ b/packages/cli/test/execute/serialize-output.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; -import { mockFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { readFile } from 'node:fs/promises'; import serializeOutput from '../../src/execute/serialize-output'; import { createMockLogger } from '@openfn/logger'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.beforeEach(() => { @@ -13,7 +15,7 @@ test.beforeEach(() => { const toString = (obj: object) => JSON.stringify(obj, null, 2); -test('output true', async (t) => { +test.serial('output true', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -24,7 +26,7 @@ test('output true', async (t) => { t.is(result, 'true'); }); -test('output false', async (t) => { +test.serial('output false', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -35,7 +37,7 @@ test('output false', async (t) => { t.is(result, 'false'); }); -test('output a number', async (t) => { +test.serial('output a number', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -46,7 +48,7 @@ test('output a number', async (t) => { t.is(result, '42'); }); -test('output null', async (t) => { +test.serial('output null', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -57,7 +59,7 @@ test('output null', async (t) => { t.is(result, 'null'); }); -test('output undefined as an empty string', async (t) => { +test.serial('output undefined as an empty string', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -68,7 +70,7 @@ test('output undefined as an empty string', async (t) => { t.is(result, ''); }); -test('output a string', async (t) => { +test.serial('output a string', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -79,7 +81,7 @@ test('output a string', async (t) => { t.is(result, '"ok"'); }); -test('output an array', async (t) => { +test.serial('output an array', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -90,7 +92,7 @@ test('output an array', async (t) => { t.is(result, toString(['ok'])); }); -test('output an object', async (t) => { +test.serial('output an object', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -101,7 +103,7 @@ test('output an object', async (t) => { t.is(result, '{}'); }); -test('exclude configuration', async (t) => { +test.serial('exclude configuration', async (t) => { const result = await serializeOutput( { outputStdout: true, @@ -117,7 +119,7 @@ test('exclude configuration', async (t) => { t.is(result, toString({ data: {}, foo: 'bar', _secret: true })); }); -test('output to file', async (t) => { +test.serial('output to file', async (t) => { mockFs({ 'out.json': '', }); diff --git a/packages/cli/test/helpers/memfs.ts b/packages/cli/test/helpers/memfs.ts deleted file mode 100644 index 5f822da12..000000000 --- a/packages/cli/test/helpers/memfs.ts +++ /dev/null @@ -1,119 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { syncBuiltinESMExports } from 'node:module'; -import { memfs, type NestedDirectoryJSON } from 'memfs'; - -/* - * Swaps node:fs for memfs while mocked. Every fs function is replaced once, - * when this module loads, with one that picks memfs or the real disk on each - * call. Libraries like glob and rimraf keep references to fs functions from - * load time, so import this before the code under test. - */ - -type FsObject = Record; - -// Tests run from the package folder, two below the repo root -const repoModules = path.resolve('../../node_modules') + path.sep; - -let mem: FsObject | undefined; -const realFds = new Set(); - -// The repo's own node_modules stay on the real disk, so that Node's module -// loader (which reads through node:fs) can still load dependencies lazily -const useReal = (target: unknown) => { - if (!mem) return true; - if (typeof target === 'number') return realFds.has(target); - if (typeof target === 'string') { - return path.resolve(target).startsWith(repoModules); - } - if (target instanceof URL) { - return path.resolve(fileURLToPath(target)).startsWith(repoModules); - } - return false; -}; - -const dispatch = ( - target: FsObject, - key: string, - getMem: () => FsObject | undefined -) => { - const real = target[key]; - const fn = (...args: any[]) => { - if (!useReal(args[0])) return getMem()![key](...args); - if (key === 'openSync') { - const fd = real(...args); - realFds.add(fd); - return fd; - } - if (key === 'open' && typeof args.at(-1) === 'function') { - const cb = args.pop(); - return real(...args, (err: Error | null, fd: number) => { - if (!err) realFds.add(fd); - cb(err, fd); - }); - } - if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); - return real(...args); - }; - if (real.native) { - fn.native = (...args: any[]) => - useReal(args[0]) ? real.native(...args) : getMem()![key](...args); - } - target[key] = fn; -}; - -// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream -// are left alone -const functionKeys = (obj: FsObject) => - Object.keys(obj).filter( - (key) => - /^[a-z]/.test(key) && - typeof obj[key] === 'function' && - Object.getOwnPropertyDescriptor(obj, key)?.writable - ); - -const realFs = fs as unknown as FsObject; -const realPromises = fs.promises as unknown as FsObject; -for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); -for (const key of functionKeys(realPromises)) { - dispatch(realPromises, key, () => mem?.promises); -} -syncBuiltinESMExports(); - -// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative -// paths in `tree` resolve against the cwd. -export const mockFs = (tree: NestedDirectoryJSON = {}) => { - restoreFs(); - const { fs: next } = memfs(tree, process.cwd()); - next.mkdirSync(process.cwd(), { recursive: true }); - next.mkdirSync(os.tmpdir(), { recursive: true }); - mem = next as unknown as FsObject; -}; - -export const restoreFs = () => { - mem = undefined; -}; - -const readReal = (target: string): NestedDirectoryJSON | Buffer => { - if (!fs.statSync(target).isDirectory()) { - return fs.readFileSync(target); - } - const dir: NestedDirectoryJSON = {}; - for (const name of fs.readdirSync(target)) { - dir[name] = readReal(path.join(target, name)); - } - return dir; -}; - -// Reads a real file or directory into a tree for mockFs, even while mocked -export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { - const current = mem; - mem = undefined; - try { - return readReal(target); - } finally { - mem = current; - } -}; diff --git a/packages/cli/test/memfs.test.ts b/packages/cli/test/memfs.test.ts index 3fe4d351a..60c8be4ce 100644 --- a/packages/cli/test/memfs.test.ts +++ b/packages/cli/test/memfs.test.ts @@ -4,7 +4,7 @@ import os from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; import { createRequire } from 'node:module'; -import { mockFs, restoreFs } from './helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; const require = createRequire(import.meta.url); @@ -33,6 +33,13 @@ test.serial('copyFileSync from node_modules writes into memfs', (t) => { } }); +test.serial('renameSync from node_modules into memfs throws EXDEV', (t) => { + mockFs(); + t.throws(() => fs.renameSync(realModule, '/moved.js'), { code: 'EXDEV' }); + restoreFs(); + t.true(fs.existsSync(realModule)); +}); + test.serial('fstatSync works on stdout while mocked', (t) => { mockFs(); t.notThrows(() => fs.fstatSync(1)); diff --git a/packages/cli/test/projects/checkout.test.ts b/packages/cli/test/projects/checkout.test.ts index 512aa2e09..bba033d7d 100644 --- a/packages/cli/test/projects/checkout.test.ts +++ b/packages/cli/test/projects/checkout.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; -import { mockFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { handler as checkoutHandler } from '../../src/projects/checkout'; import fs from 'fs'; import { jsonToYaml, Workspace, yamlToJson } from '@openfn/project'; +test.afterEach(restoreFs); + test.beforeEach(() => { mockFs({ '/ws/workflows': {}, @@ -401,6 +403,7 @@ test.serial.skip('checkout: switch with alias and domain', async (t) => { }); test.serial('respect openfn.yaml settings', async (t) => { + restoreFs(); mockFs({ '/ws1/w': {}, '/ws1/openfn.yaml': jsonToYaml({ @@ -548,6 +551,7 @@ uuid: test.serial( 'checkout: removes old workflow directory when workflow is renamed on server', async (t) => { + restoreFs(); mockFs({ '/ws3/workflows/old-workflow': { 'old-workflow.yaml': jsonToYaml({ @@ -591,6 +595,7 @@ test.serial( test.serial( 'checkout: removes old step file when step is renamed on server', async (t) => { + restoreFs(); mockFs({ '/ws4/workflows/my-workflow': { 'my-workflow.yaml': jsonToYaml({ @@ -633,6 +638,7 @@ test.serial( ); test.serial('checkout: creates credentials.yaml', async (t) => { + restoreFs(); mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', @@ -684,6 +690,7 @@ test.serial('checkout: creates credentials.yaml', async (t) => { }); test.serial('checkout: do not create credentials.yaml', async (t) => { + restoreFs(); mockFs({ '/ws2/workflows': {}, '/ws2/openfn.yaml': '', @@ -733,6 +740,7 @@ test.serial('checkout: do not create credentials.yaml', async (t) => { test.serial( 'checkout: removes workflow directory when workflow is deleted on server', async (t) => { + restoreFs(); mockFs({ '/ws5/workflows/workflow-a': { 'workflow-a.yaml': jsonToYaml({ @@ -779,6 +787,7 @@ test.serial( test.serial( 'checkout: writes fetched collections into resources.yaml', async (t) => { + restoreFs(); mockFs({ '/ws6/workflows': {}, '/ws6/openfn.yaml': '', @@ -951,6 +960,7 @@ workflows: test.serial( 'Checkout unrelated bar from unrelated project foo without divergence warning', async (t) => { + restoreFs(); mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, @@ -999,6 +1009,7 @@ test.serial( test.serial( 'Checkout unrelated foo from unrelated project bar without divergence warning', async (t) => { + restoreFs(); mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, @@ -1047,6 +1058,7 @@ test.serial( test.serial( 'If the checked out project has diverged from the tracked version, show a divergence warning on checkout', async (t) => { + restoreFs(); mockFs({ '/tmp/openfn.yaml': '', '/tmp/.projects/main@server.yaml': foo, diff --git a/packages/cli/test/projects/clean.test.ts b/packages/cli/test/projects/clean.test.ts index a526bd1d2..e4636462d 100644 --- a/packages/cli/test/projects/clean.test.ts +++ b/packages/cli/test/projects/clean.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { handler as cleanHandler } from '../../src/projects/clean'; import fs from 'fs'; import { jsonToYaml } from '@openfn/project'; +test.afterEach(restoreFs); + const logger = createMockLogger('', { level: 'debug' }); const projectStateFile = jsonToYaml({ @@ -51,7 +53,6 @@ test.beforeEach(() => { }); test.afterEach(() => { - restoreFs(); logger._reset(); }); @@ -94,6 +95,7 @@ test.serial('clean: aborts if user declines confirmation', async (t) => { test.serial( 'clean: throws if no active project found in workspace', async (t) => { + restoreFs(); mockFs({ '/ws/workflows': {} }); await t.throwsAsync( @@ -111,6 +113,7 @@ test.serial( 'does not delete anything if it is not known which project is checked out', async (t) => { // two tracked projects, but no checkout metadata anywhere + restoreFs(); mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', @@ -133,6 +136,7 @@ test.serial( 'does not delete anything if there is no active project to restore', async (t) => { // a workspace with no projects and no checkout metadata at all + restoreFs(); mockFs({ '/ws/workflows/old-workflow': { 'old-job.js': 'fn(s => s)' }, '/ws/openfn.yaml': '', diff --git a/packages/cli/test/projects/credentials-helpers.test.ts b/packages/cli/test/projects/credentials-helpers.test.ts index 86e1103f3..60a4dd963 100644 --- a/packages/cli/test/projects/credentials-helpers.test.ts +++ b/packages/cli/test/projects/credentials-helpers.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; import fs from 'node:fs'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import Project, { yamlToJson } from '@openfn/project'; @@ -18,11 +18,7 @@ import { default as parseCredentialsOption, } from '../../src/projects/credentials-helpers'; -test.afterEach(() => { - try { - restoreFs(); - } catch {} -}); +test.afterEach(restoreFs); const createProject = (credentials: any[], steps: any[]) => new Project({ @@ -38,7 +34,7 @@ const step = (id: string, configuration?: string) => ({ configuration, }); -test('byNone drops every credential', (t) => { +test.serial('byNone drops every credential', (t) => { const project = createProject( [{ name: 'a', owner: 'joe@openfn.org' }], [step('x', 'joe@openfn.org|a')] @@ -52,7 +48,7 @@ test('byNone drops every credential', (t) => { t.is(project.workflows[0].steps[0].configuration as any, null); }); -test('byAll keeps every credential and reference untouched', (t) => { +test.serial('byAll keeps every credential and reference untouched', (t) => { const project = createProject( [{ name: 'a', owner: 'joe@openfn.org' }], [step('x', 'joe@openfn.org|a')] @@ -64,7 +60,7 @@ test('byAll keeps every credential and reference untouched', (t) => { t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); }); -test('byPrune drops credentials not referenced by any step', (t) => { +test.serial('byPrune drops credentials not referenced by any step', (t) => { const project = createProject( [ { name: 'a', owner: 'joe@openfn.org' }, @@ -79,7 +75,7 @@ test('byPrune drops credentials not referenced by any step', (t) => { t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); }); -test('byMap drops credentials not named in the map', (t) => { +test.serial('byMap drops credentials not named in the map', (t) => { const project = createProject( [ { name: 'a', owner: 'joe@openfn.org' }, @@ -95,207 +91,255 @@ test('byMap drops credentials not named in the map', (t) => { t.is(project.workflows[0].steps[1].configuration as any, null); }); -test('byMap renames a credential and rewrites its step references', (t) => { - const project = createProject( - [{ name: 'c', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|c')] - ); - - remapCredentials( - project, - byMap({ c: { name: 'c', owner: 'other@openfn.org' } }) - ); - - t.deepEqual(project.credentials, [ - { name: 'c', owner: 'other@openfn.org' }, - ]); - t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|c'); -}); - -test('byCredentialsFile drops credentials not listed in the file, keeps listed ones', (t) => { - const project = createProject( - [ - { name: 'a', owner: 'joe@openfn.org' }, - { name: 'b', owner: 'joe@openfn.org' }, - ], - [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] - ); - - remapCredentials( - project, - byCredentialsFile({ 'joe@openfn.org|a': undefined as any }) - ); - - t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); - t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); - t.is(project.workflows[0].steps[1].configuration as any, null); -}); - -test('byCredentialsFile renames a credential per its alias entry', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - remapCredentials( - project, - byCredentialsFile({ - 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, - }) - ); - - t.deepEqual(project.credentials, [ - { name: 'renamed', owner: 'other@openfn.org' }, - ]); - t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|renamed'); -}); - -test('loadCredentialsMapFromFile: an entry with no alias just marks inclusion', (t) => { - mockFs({ - '/ws/credentials.yaml': `joe@openfn.org|a: +test.serial( + 'byMap renames a credential and rewrites its step references', + (t) => { + const project = createProject( + [{ name: 'c', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|c')] + ); + + remapCredentials( + project, + byMap({ c: { name: 'c', owner: 'other@openfn.org' } }) + ); + + t.deepEqual(project.credentials, [ + { name: 'c', owner: 'other@openfn.org' }, + ]); + t.is(project.workflows[0].steps[0].configuration, 'other@openfn.org|c'); + } +); + +test.serial( + 'byCredentialsFile drops credentials not listed in the file, keeps listed ones', + (t) => { + const project = createProject( + [ + { name: 'a', owner: 'joe@openfn.org' }, + { name: 'b', owner: 'joe@openfn.org' }, + ], + [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] + ); + + remapCredentials( + project, + byCredentialsFile({ 'joe@openfn.org|a': undefined as any }) + ); + + t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); + t.is(project.workflows[0].steps[0].configuration, 'joe@openfn.org|a'); + t.is(project.workflows[0].steps[1].configuration as any, null); + } +); + +test.serial( + 'byCredentialsFile renames a credential per its alias entry', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + remapCredentials( + project, + byCredentialsFile({ + 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, + }) + ); + + t.deepEqual(project.credentials, [ + { name: 'renamed', owner: 'other@openfn.org' }, + ]); + t.is( + project.workflows[0].steps[0].configuration, + 'other@openfn.org|renamed' + ); + } +); + +test.serial( + 'loadCredentialsMapFromFile: an entry with no alias just marks inclusion', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, - }); - - const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); - t.deepEqual(map, { 'joe@openfn.org|a': undefined as any }); -}); - -test('loadCredentialsMapFromFile: an entry with an alias carries it through', (t) => { - mockFs({ - '/ws/credentials.yaml': `joe@openfn.org|a: + }); + + const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); + t.deepEqual(map, { 'joe@openfn.org|a': undefined as any }); + } +); + +test.serial( + 'loadCredentialsMapFromFile: an entry with an alias carries it through', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser alias: other@openfn.org|renamed `, - }); - - const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); - t.deepEqual(map, { - 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, - }); -}); + }); -test('getCredentialsVisitor: a credentials file path syncs only what it lists', (t) => { - mockFs({ - '/ws/credentials.yaml': `joe@openfn.org|a: + const map = loadCredentialsMapFromFile('/ws/credentials.yaml'); + t.deepEqual(map, { + 'joe@openfn.org|a': { name: 'renamed', owner: 'other@openfn.org' }, + }); + } +); + +test.serial( + 'getCredentialsVisitor: a credentials file path syncs only what it lists', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|a: user: someuser `, - }); - - const project = createProject( - [ - { name: 'a', owner: 'joe@openfn.org' }, - { name: 'b', owner: 'joe@openfn.org' }, - ], - [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] - ); - - remapCredentials( - project, - getCredentialsVisitor(project, 'credentials.yaml', '/ws') - ); - - t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); - t.is(project.workflows[0].steps[1].configuration as any, null); -}); - -test('getCredentialsVisitor: throws when the --credentials map names a credential not in the project', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - const error = t.throws(() => - getCredentialsVisitor(project, { typo: { name: 'typo' } }) - ); - - t.regex(error!.message, /typo/); -}); + }); -test('getCredentialsVisitor: throws when a credentials.yaml file names a credential not in the project', (t) => { - mockFs({ - '/ws/credentials.yaml': `joe@openfn.org|not-a-real-credential: + const project = createProject( + [ + { name: 'a', owner: 'joe@openfn.org' }, + { name: 'b', owner: 'joe@openfn.org' }, + ], + [step('x', 'joe@openfn.org|a'), step('y', 'joe@openfn.org|b')] + ); + + remapCredentials( + project, + getCredentialsVisitor(project, 'credentials.yaml', '/ws') + ); + + t.deepEqual(project.credentials, [{ name: 'a', owner: 'joe@openfn.org' }]); + t.is(project.workflows[0].steps[1].configuration as any, null); + } +); + +test.serial( + 'getCredentialsVisitor: throws when the --credentials map names a credential not in the project', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + const error = t.throws(() => + getCredentialsVisitor(project, { typo: { name: 'typo' } }) + ); + + t.regex(error!.message, /typo/); + } +); + +test.serial( + 'getCredentialsVisitor: throws when a credentials.yaml file names a credential not in the project', + (t) => { + mockFs({ + '/ws/credentials.yaml': `joe@openfn.org|not-a-real-credential: user: someuser `, - }); - - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - const error = t.throws(() => - getCredentialsVisitor(project, 'credentials.yaml', '/ws') - ); - - t.regex(error!.message, /joe@openfn\.org\|not-a-real-credential/); -}); - -test('getCredentialsVisitor: does not throw when every named credential exists', (t) => { - const project = createProject( - [{ name: 'a', owner: 'joe@openfn.org' }], - [step('x', 'joe@openfn.org|a')] - ); - - t.notThrows(() => - getCredentialsVisitor(project, { a: { name: 'renamed' } }) - ); -}); - -test('parseCredentialsOption: a credentials file path passes through unchanged', (t) => { - t.is(parseCredentialsOption('./credentials.yaml'), './credentials.yaml'); - t.is(parseCredentialsOption('creds.yml'), 'creds.yml'); - t.is(parseCredentialsOption('creds.json'), 'creds.json'); -}); - -test('parseCredentialsOption: none', (t) => { + }); + + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + const error = t.throws(() => + getCredentialsVisitor(project, 'credentials.yaml', '/ws') + ); + + t.regex(error!.message, /joe@openfn\.org\|not-a-real-credential/); + } +); + +test.serial( + 'getCredentialsVisitor: does not throw when every named credential exists', + (t) => { + const project = createProject( + [{ name: 'a', owner: 'joe@openfn.org' }], + [step('x', 'joe@openfn.org|a')] + ); + + t.notThrows(() => + getCredentialsVisitor(project, { a: { name: 'renamed' } }) + ); + } +); + +test.serial( + 'parseCredentialsOption: a credentials file path passes through unchanged', + (t) => { + t.is(parseCredentialsOption('./credentials.yaml'), './credentials.yaml'); + t.is(parseCredentialsOption('creds.yml'), 'creds.yml'); + t.is(parseCredentialsOption('creds.json'), 'creds.json'); + } +); + +test.serial('parseCredentialsOption: none', (t) => { t.is(parseCredentialsOption('none'), 'none'); }); -test('parseCredentialsOption: prune', (t) => { +test.serial('parseCredentialsOption: prune', (t) => { t.is(parseCredentialsOption('prune'), 'prune'); }); -test('parseCredentialsOption: all', (t) => { +test.serial('parseCredentialsOption: all', (t) => { t.is(parseCredentialsOption('all'), 'all'); }); -test('parseCredentialsOption: a single credential name with no alias', (t) => { - const result = parseCredentialsOption('a'); - t.deepEqual(result, { a: { name: 'a' } }); -}); - -test('parseCredentialsOption: a comma separated list of credential names', (t) => { - const result = parseCredentialsOption('a,b,c'); - t.deepEqual(result, { - a: { name: 'a' }, - b: { name: 'b' }, - c: { name: 'c' }, - }); -}); - -test('parseCredentialsOption: a credential mapped to a new name and owner', (t) => { - const result = parseCredentialsOption('c=joe@openfn.org|c'); - t.deepEqual(result, { - c: { name: 'c', owner: 'joe@openfn.org' }, - }); -}); - -test('parseCredentialsOption: a mix of plain names and aliased names', (t) => { - const result = parseCredentialsOption('a,b,c=joe@openfn.org|c'); - t.deepEqual(result, { - a: { name: 'a' }, - b: { name: 'b' }, - c: { name: 'c', owner: 'joe@openfn.org' }, - }); -}); - -test('parseCredentialsOption: an alias with a new name but no owner', (t) => { - const result = parseCredentialsOption('a=renamed'); - t.deepEqual(result, { - a: { name: 'renamed', owner: undefined }, - }); -}); +test.serial( + 'parseCredentialsOption: a single credential name with no alias', + (t) => { + const result = parseCredentialsOption('a'); + t.deepEqual(result, { a: { name: 'a' } }); + } +); + +test.serial( + 'parseCredentialsOption: a comma separated list of credential names', + (t) => { + const result = parseCredentialsOption('a,b,c'); + t.deepEqual(result, { + a: { name: 'a' }, + b: { name: 'b' }, + c: { name: 'c' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: a credential mapped to a new name and owner', + (t) => { + const result = parseCredentialsOption('c=joe@openfn.org|c'); + t.deepEqual(result, { + c: { name: 'c', owner: 'joe@openfn.org' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: a mix of plain names and aliased names', + (t) => { + const result = parseCredentialsOption('a,b,c=joe@openfn.org|c'); + t.deepEqual(result, { + a: { name: 'a' }, + b: { name: 'b' }, + c: { name: 'c', owner: 'joe@openfn.org' }, + }); + } +); + +test.serial( + 'parseCredentialsOption: an alias with a new name but no owner', + (t) => { + const result = parseCredentialsOption('a=renamed'); + t.deepEqual(result, { + a: { name: 'renamed', owner: undefined }, + }); + } +); const baseWorkflow = (steps: any[]) => ({ id: 'wf', @@ -304,7 +348,7 @@ const baseWorkflow = (steps: any[]) => ({ steps, }); -test('findCredentialIds: inline string references', (t) => { +test.serial('findCredentialIds: inline string references', (t) => { const project = new Project({ id: 'p', workflows: [ @@ -320,7 +364,7 @@ test('findCredentialIds: inline string references', (t) => { t.deepEqual(ids, ['owner|cred']); }); -test('findCredentialIds: ignores duplicate references', (t) => { +test.serial('findCredentialIds: ignores duplicate references', (t) => { const project = new Project({ id: 'p', workflows: [ @@ -332,7 +376,7 @@ test('findCredentialIds: ignores duplicate references', (t) => { t.deepEqual(findCredentialIds(project), ['same']); }); -test('createProjectCredentials: creates credential yaml file', (t) => { +test.serial('createProjectCredentials: creates credential yaml file', (t) => { mockFs({ '/ws': {} }); const project = new Project( @@ -352,32 +396,35 @@ test('createProjectCredentials: creates credential yaml file', (t) => { t.deepEqual(doc, { 'new-id': {} }); }); -test('createProjectCredentials: preserves existing credentials and adds missing ones', (t) => { - mockFs({ - '/ws': {}, - '/ws/credentials.yaml': `existing: +test.serial( + 'createProjectCredentials: preserves existing credentials and adds missing ones', + (t) => { + mockFs({ + '/ws': {}, + '/ws/credentials.yaml': `existing: password: secret `, - }); - - const project = new Project( - { - id: 'p', - workflows: [ - baseWorkflow([ - { id: 'j', configuration: 'existing' }, - { id: 'k', configuration: 'brand-new' }, - ]), - ], - } as any, - { credentials: 'credentials.yaml' } - ); - - createProjectCredentials('/ws', project); - - const doc = yamlToJson( - fs.readFileSync('/ws/credentials.yaml', 'utf8') - ) as any; - t.is(doc.existing.password, 'secret'); - t.deepEqual(doc['brand-new'], {}); -}); + }); + + const project = new Project( + { + id: 'p', + workflows: [ + baseWorkflow([ + { id: 'j', configuration: 'existing' }, + { id: 'k', configuration: 'brand-new' }, + ]), + ], + } as any, + { credentials: 'credentials.yaml' } + ); + + createProjectCredentials('/ws', project); + + const doc = yamlToJson( + fs.readFileSync('/ws/credentials.yaml', 'utf8') + ) as any; + t.is(doc.existing.password, 'secret'); + t.deepEqual(doc['brand-new'], {}); + } +); diff --git a/packages/cli/test/projects/deploy.test.ts b/packages/cli/test/projects/deploy.test.ts index 3d3eb9607..87e49e8d9 100644 --- a/packages/cli/test/projects/deploy.test.ts +++ b/packages/cli/test/projects/deploy.test.ts @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises'; import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import Project, { generateWorkflow, @@ -30,6 +30,8 @@ import { import { checkout } from '../../src/projects'; import { rimraf } from 'rimraf'; +test.afterEach(restoreFs); + let server: any; let strictServer: any; const logger = createMockLogger(undefined, { level: 'debug' }); @@ -68,7 +70,6 @@ test.beforeEach(() => { server.reset(); server.addProject(myProject_v1); logger._reset(); - restoreFs(); }); test.serial( @@ -921,28 +922,31 @@ test.serial( } ); -test('printRichDiff: should report no changes for identical projects', (t) => { - const wf = generateWorkflow('@id a trigger-x'); +test.serial( + 'printRichDiff: should report no changes for identical projects', + (t) => { + const wf = generateWorkflow('@id a trigger-x'); - const local = new Project({ - name: 'local', - workflows: [wf], - }); + const local = new Project({ + name: 'local', + workflows: [wf], + }); - const remote = new Project({ - name: 'remote', - workflows: [wf], - }); + const remote = new Project({ + name: 'remote', + workflows: [wf], + }); - const diffs = printRichDiff(local, remote, [], logger); - t.is(diffs.length, 0); + const diffs = printRichDiff(local, remote, [], logger); + t.is(diffs.length, 0); - const { message, level } = logger._parse(logger._last); - t.is(level, 'info'); - t.is(message, 'No workflow changes detected'); -}); + const { message, level } = logger._parse(logger._last); + t.is(level, 'info'); + t.is(message, 'No workflow changes detected'); + } +); -test('printRichDiff: should report changed workflow', (t) => { +test.serial('printRichDiff: should report changed workflow', (t) => { const wfRemote = generateWorkflow('@id a trigger-x'); const wfLocal = generateWorkflow('@id a trigger-y'); @@ -963,7 +967,7 @@ test('printRichDiff: should report changed workflow', (t) => { t.truthy(logger._find('always', /: changed/i)); }); -test('printRichDiff: should report added workflow', (t) => { +test.serial('printRichDiff: should report added workflow', (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -984,7 +988,7 @@ test('printRichDiff: should report added workflow', (t) => { t.truthy(logger._find('always', /: added/i)); }); -test('printRichDiff: should report removed workflow', (t) => { +test.serial('printRichDiff: should report removed workflow', (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -1005,45 +1009,48 @@ test('printRichDiff: should report removed workflow', (t) => { t.truthy(logger._find('always', /: deleted/i)); }); -test('printRichDiff: should report mix of added, changed, and removed workflows', (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2Remote = generateWorkflow('@id b trigger-y'); - const wf2Local = generateWorkflow('@id b trigger-different'); - const wf3 = generateWorkflow('@id c trigger-z'); - const wf4 = generateWorkflow('@id d trigger-w'); - - const local = new Project({ - name: 'local', - workflows: [wf1, wf2Local, wf4], // has a, b (changed), d (new) - }); +test.serial( + 'printRichDiff: should report mix of added, changed, and removed workflows', + (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2Remote = generateWorkflow('@id b trigger-y'); + const wf2Local = generateWorkflow('@id b trigger-different'); + const wf3 = generateWorkflow('@id c trigger-z'); + const wf4 = generateWorkflow('@id d trigger-w'); + + const local = new Project({ + name: 'local', + workflows: [wf1, wf2Local, wf4], // has a, b (changed), d (new) + }); - const remote = new Project({ - name: 'remote', - workflows: [wf1, wf2Remote, wf3], // has a, b, c - }); + const remote = new Project({ + name: 'remote', + workflows: [wf1, wf2Remote, wf3], // has a, b, c + }); - const diffs = printRichDiff(local, remote, [], logger); - t.is(diffs.length, 3); + const diffs = printRichDiff(local, remote, [], logger); + t.is(diffs.length, 3); - t.deepEqual( - diffs.find((d) => d.id === 'b'), - { id: 'b', type: 'changed' } - ); - t.deepEqual( - diffs.find((d) => d.id === 'c'), - { id: 'c', type: 'removed' } - ); - t.deepEqual( - diffs.find((d) => d.id === 'd'), - { id: 'd', type: 'added' } - ); + t.deepEqual( + diffs.find((d) => d.id === 'b'), + { id: 'b', type: 'changed' } + ); + t.deepEqual( + diffs.find((d) => d.id === 'c'), + { id: 'c', type: 'removed' } + ); + t.deepEqual( + diffs.find((d) => d.id === 'd'), + { id: 'd', type: 'added' } + ); - t.truthy(logger._find('always', /: added/i)); - t.truthy(logger._find('always', /: changed/i)); - t.truthy(logger._find('always', /: deleted/i)); -}); + t.truthy(logger._find('always', /: added/i)); + t.truthy(logger._find('always', /: changed/i)); + t.truthy(logger._find('always', /: deleted/i)); + } +); -test('hasRemoteDiverged: 1 workflow, no diverged', (t) => { +test.serial('hasRemoteDiverged: 1 workflow, no diverged', (t) => { const local = { workflows: [ { @@ -1068,7 +1075,7 @@ test('hasRemoteDiverged: 1 workflow, no diverged', (t) => { t.falsy(diverged); }); -test('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { +test.serial('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { const local = { workflows: [ { @@ -1093,21 +1100,24 @@ test('hasRemoteDiverged: 1 workflow, 1 diverged', (t) => { t.deepEqual(diverged, ['w']); }); -test('collectionsChanged: false when the same names are on both sides', (t) => { - const local = { - collections: [{ name: 'a' }, { name: 'b' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-a', name: 'a' }, - { uuid: 'uuid-b', name: 'b' }, - ], - } as unknown as Project; +test.serial( + 'collectionsChanged: false when the same names are on both sides', + (t) => { + const local = { + collections: [{ name: 'a' }, { name: 'b' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-a', name: 'a' }, + { uuid: 'uuid-b', name: 'b' }, + ], + } as unknown as Project; - t.false(collectionsChanged(local, remote)); -}); + t.false(collectionsChanged(local, remote)); + } +); -test('collectionsChanged: true when a name was added locally', (t) => { +test.serial('collectionsChanged: true when a name was added locally', (t) => { const local = { collections: [{ name: 'a' }, { name: 'b' }], } as unknown as Project; @@ -1118,7 +1128,7 @@ test('collectionsChanged: true when a name was added locally', (t) => { t.true(collectionsChanged(local, remote)); }); -test('collectionsChanged: true when a name was removed locally', (t) => { +test.serial('collectionsChanged: true when a name was removed locally', (t) => { const local = { collections: [{ name: 'a' }], } as unknown as Project; @@ -1132,63 +1142,78 @@ test('collectionsChanged: true when a name was removed locally', (t) => { t.true(collectionsChanged(local, remote)); }); -test('collectionsChanged: false when collections are not managed locally', (t) => { - // undefined means "leave alone", unlike an empty list - const local = {} as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; - - t.false(collectionsChanged(local, remote)); -}); - -test('collectionsChanged: true when every collection was removed locally', (t) => { - const local = { collections: [] } as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; - - t.true(collectionsChanged(local, remote)); -}); +test.serial( + 'collectionsChanged: false when collections are not managed locally', + (t) => { + // undefined means "leave alone", unlike an empty list + const local = {} as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.false(collectionsChanged(local, remote)); + } +); -test('deletedCollections: nothing to delete when collections are not managed locally', (t) => { - const merged = {} as unknown as Project; - const remote = { - collections: [{ uuid: 'uuid-a', name: 'a' }], - } as unknown as Project; +test.serial( + 'collectionsChanged: true when every collection was removed locally', + (t) => { + const local = { collections: [] } as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.true(collectionsChanged(local, remote)); + } +); - t.deepEqual(deletedCollections(merged, remote), []); -}); +test.serial( + 'deletedCollections: nothing to delete when collections are not managed locally', + (t) => { + const merged = {} as unknown as Project; + const remote = { + collections: [{ uuid: 'uuid-a', name: 'a' }], + } as unknown as Project; + + t.deepEqual(deletedCollections(merged, remote), []); + } +); -test('deletedCollections: flags a remote name missing from the merged project', (t) => { - const merged = { - collections: [{ name: 'keep-me' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-keep', name: 'keep-me' }, - { uuid: 'uuid-remove', name: 'remove-me' }, - ], - } as unknown as Project; +test.serial( + 'deletedCollections: flags a remote name missing from the merged project', + (t) => { + const merged = { + collections: [{ name: 'keep-me' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-keep', name: 'keep-me' }, + { uuid: 'uuid-remove', name: 'remove-me' }, + ], + } as unknown as Project; - t.deepEqual(deletedCollections(merged, remote), [ - { id: 'uuid-remove', name: 'remove-me', delete: true }, - ]); -}); + t.deepEqual(deletedCollections(merged, remote), [ + { id: 'uuid-remove', name: 'remove-me', delete: true }, + ]); + } +); -test('deletedCollections: nothing to delete when every remote name survives', (t) => { - const merged = { - collections: [{ name: 'a' }, { name: 'b' }], - } as unknown as Project; - const remote = { - collections: [ - { uuid: 'uuid-a', name: 'a' }, - { uuid: 'uuid-b', name: 'b' }, - ], - } as unknown as Project; +test.serial( + 'deletedCollections: nothing to delete when every remote name survives', + (t) => { + const merged = { + collections: [{ name: 'a' }, { name: 'b' }], + } as unknown as Project; + const remote = { + collections: [ + { uuid: 'uuid-a', name: 'a' }, + { uuid: 'uuid-b', name: 'b' }, + ], + } as unknown as Project; - t.deepEqual(deletedCollections(merged, remote), []); -}); + t.deepEqual(deletedCollections(merged, remote), []); + } +); const channel = { name: 'my-channel', @@ -1196,16 +1221,19 @@ const channel = { enabled: true, }; -test('channelsChanged: false when there is no local resources.yaml', (t) => { - const local = { channels: undefined } as unknown as Project; - const remote = { - channels: [{ ...channel, id: 'chan-1' }], - } as unknown as Project; - - t.false(channelsChanged(local, remote)); -}); +test.serial( + 'channelsChanged: false when there is no local resources.yaml', + (t) => { + const local = { channels: undefined } as unknown as Project; + const remote = { + channels: [{ ...channel, id: 'chan-1' }], + } as unknown as Project; + + t.false(channelsChanged(local, remote)); + } +); -test('channelsChanged: true when a channel is edited locally', (t) => { +test.serial('channelsChanged: true when a channel is edited locally', (t) => { const local = { channels: [{ ...channel, enabled: false }], } as unknown as Project; @@ -1216,19 +1244,26 @@ test('channelsChanged: true when a channel is edited locally', (t) => { t.true(channelsChanged(local, remote)); }); -test('channelsChanged: false when a credential name matches the remote uuid', (t) => { - const local = { - channels: [{ ...channel, destination_credential_id: 'me@openfn.org|cred' }], - } as unknown as Project; - const remote = { - credentials: [{ uuid: 'cred-uuid', name: 'cred', owner: 'me@openfn.org' }], - channels: [ - { ...channel, id: 'chan-1', destination_credential_id: 'cred-uuid' }, - ], - } as unknown as Project; +test.serial( + 'channelsChanged: false when a credential name matches the remote uuid', + (t) => { + const local = { + channels: [ + { ...channel, destination_credential_id: 'me@openfn.org|cred' }, + ], + } as unknown as Project; + const remote = { + credentials: [ + { uuid: 'cred-uuid', name: 'cred', owner: 'me@openfn.org' }, + ], + channels: [ + { ...channel, id: 'chan-1', destination_credential_id: 'cred-uuid' }, + ], + } as unknown as Project; - t.false(channelsChanged(local, remote)); -}); + t.false(channelsChanged(local, remote)); + } +); const diffChannel = (name: string, extra = {}) => ({ name, @@ -1237,7 +1272,7 @@ const diffChannel = (name: string, extra = {}) => ({ ...extra, }); -test('printRichDiff: should report a channel-only change', (t) => { +test.serial('printRichDiff: should report a channel-only change', (t) => { const local = new Project({ name: 'local', workflows: [], @@ -1252,7 +1287,7 @@ test('printRichDiff: should report a channel-only change', (t) => { t.truthy(logger._find('always', /one: added/)); }); -test('printRichDiff: should report removed channels', (t) => { +test.serial('printRichDiff: should report removed channels', (t) => { const local = new Project({ name: 'local', workflows: [], channels: [] }); const remote = new Project({ name: 'remote', @@ -1265,63 +1300,76 @@ test('printRichDiff: should report removed channels', (t) => { t.truthy(logger._find('always', /one: removed/)); }); -test('printRichDiff: should report added and removed collections', (t) => { - const local = new Project({ - name: 'local', - workflows: [], - collections: [{ name: 'keep-me' }, { name: 'new-one' }], - }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [ - { uuid: 'a', name: 'keep-me' }, - { uuid: 'b', name: 'old-one' }, - ], - }); +test.serial( + 'printRichDiff: should report added and removed collections', + (t) => { + const local = new Project({ + name: 'local', + workflows: [], + collections: [{ name: 'keep-me' }, { name: 'new-one' }], + }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [ + { uuid: 'a', name: 'keep-me' }, + { uuid: 'b', name: 'old-one' }, + ], + }); - printRichDiff(local, remote, [], logger); + printRichDiff(local, remote, [], logger); - t.truthy(logger._find('always', /following changes to the remote project/)); - t.truthy(logger._find('always', /Collections:/)); - t.truthy(logger._find('always', /new-one: added/)); - t.truthy(logger._find('always', /old-one: removed/)); -}); + t.truthy(logger._find('always', /following changes to the remote project/)); + t.truthy(logger._find('always', /Collections:/)); + t.truthy(logger._find('always', /new-one: added/)); + t.truthy(logger._find('always', /old-one: removed/)); + } +); -test('printRichDiff: should show every collection as removed when all are deleted', (t) => { - const local = new Project({ name: 'local', workflows: [], collections: [] }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [ - { uuid: 'a', name: 'one' }, - { uuid: 'b', name: 'two' }, - ], - }); +test.serial( + 'printRichDiff: should show every collection as removed when all are deleted', + (t) => { + const local = new Project({ + name: 'local', + workflows: [], + collections: [], + }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [ + { uuid: 'a', name: 'one' }, + { uuid: 'b', name: 'two' }, + ], + }); - printRichDiff(local, remote, [], logger); + printRichDiff(local, remote, [], logger); - t.truthy(logger._find('always', /one: removed/)); - t.truthy(logger._find('always', /two: removed/)); -}); + t.truthy(logger._find('always', /one: removed/)); + t.truthy(logger._find('always', /two: removed/)); + } +); -test('printRichDiff: should not report collections that are not managed locally', (t) => { - const local = new Project({ name: 'local', workflows: [] }); - const remote = new Project({ - name: 'remote', - workflows: [], - collections: [{ uuid: 'a', name: 'one' }], - }); +test.serial( + 'printRichDiff: should not report collections that are not managed locally', + (t) => { + const local = new Project({ name: 'local', workflows: [] }); + const remote = new Project({ + name: 'remote', + workflows: [], + collections: [{ uuid: 'a', name: 'one' }], + }); - // use a private logger: an absence check isn't safe on the shared one - const privateLogger = createMockLogger(undefined, { level: 'debug' }); - printRichDiff(local, remote, [], privateLogger); + // use a private logger: an absence check isn't safe on the shared one + const privateLogger = createMockLogger(undefined, { level: 'debug' }); + printRichDiff(local, remote, [], privateLogger); - t.falsy(privateLogger._find('always', /Collections:/)); - t.truthy(privateLogger._find('info', /No workflow changes detected/)); -}); + t.falsy(privateLogger._find('always', /Collections:/)); + t.truthy(privateLogger._find('info', /No workflow changes detected/)); + } +); -test('printRichDiff: should list changed channel fields', (t) => { +test.serial('printRichDiff: should list changed channel fields', (t) => { const local = new Project({ name: 'local', workflows: [], diff --git a/packages/cli/test/projects/ensure-checkout.test.ts b/packages/cli/test/projects/ensure-checkout.test.ts index 37b121278..5dab8f451 100644 --- a/packages/cli/test/projects/ensure-checkout.test.ts +++ b/packages/cli/test/projects/ensure-checkout.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import { createMockLogger } from '@openfn/logger'; import Project, { Workspace, yamlToJson } from '@openfn/project'; @@ -9,10 +9,11 @@ import ensureCheckout, { getDefaultProject, } from '../../src/projects/ensure-checkout'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - restoreFs(); logger._reset(); }); @@ -258,29 +259,32 @@ test.serial('still prompts if interactive, even if not required', async (t) => { const projects = (aliases: string[]) => aliases.map((alias) => ({ alias })) as unknown as Project[]; -test('getDefaultProject: prefers the alias matching the branch', (t) => { +test.serial('getDefaultProject: prefers the alias matching the branch', (t) => { const result = getDefaultProject(projects(['main', 'staging']), 'staging'); t.is(result.alias, 'staging'); }); -test('getDefaultProject: defaults to main if there is no branch', (t) => { - t.is(getDefaultProject(projects(['a', 'main', 'b'])).alias, 'main'); -}); +test.serial( + 'getDefaultProject: defaults to main if there is no branch', + (t) => { + t.is(getDefaultProject(projects(['a', 'main', 'b'])).alias, 'main'); + } +); -test('getDefaultProject: falls back to the first project', (t) => { +test.serial('getDefaultProject: falls back to the first project', (t) => { t.is(getDefaultProject(projects(['a', 'b']), 'feature').alias, 'a'); }); const described = (props: object) => describeProject(props as Project); -test('describeProject: alias with id and uuid', (t) => { +test.serial('describeProject: alias with id and uuid', (t) => { t.is( described({ alias: 'staging', id: 'my-project', openfn: { uuid: 'abcd' } }), 'staging (my-project | abcd)' ); }); -test('describeProject: copes with a missing uuid or id', (t) => { +test.serial('describeProject: copes with a missing uuid or id', (t) => { t.is( described({ alias: 'staging', id: 'my-project' }), 'staging (my-project)' @@ -291,10 +295,13 @@ test('describeProject: copes with a missing uuid or id', (t) => { ); }); -test('describeProject: just the alias if there is no id or uuid', (t) => { - t.is(described({ alias: 'staging' }), 'staging'); -}); +test.serial( + 'describeProject: just the alias if there is no id or uuid', + (t) => { + t.is(described({ alias: 'staging' }), 'staging'); + } +); -test('describeProject: copes with a missing alias', (t) => { +test.serial('describeProject: copes with a missing alias', (t) => { t.is(described({ id: 'my-project' }), '(no alias) (my-project)'); }); diff --git a/packages/cli/test/projects/fetch.test.ts b/packages/cli/test/projects/fetch.test.ts index 35659ea3a..1227ccbfb 100644 --- a/packages/cli/test/projects/fetch.test.ts +++ b/packages/cli/test/projects/fetch.test.ts @@ -1,12 +1,14 @@ import { readFile, writeFile } from 'node:fs/promises'; import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; import { handler as fetchHandler } from '../../src/projects/fetch'; import { myProject_v1, myProject_yaml } from './fixtures'; +test.afterEach(restoreFs); + const logger = createMockLogger('', { level: 'debug' }); const ENDPOINT = 'https://app.openfn.org'; @@ -46,10 +48,6 @@ test.beforeEach(() => { logger._reset(); }); -test.afterEach(() => { - restoreFs(); -}); - test.serial('fetch by UUID to default new alias', async (t) => { t.throwsAsync(() => readFile(getYamlPath('main'), 'utf-8')); @@ -483,6 +481,7 @@ test.serial( .replace(' - a', ' - z'); // change the local history to be incompatible // Make it look like we've checked out the project + restoreFs(); mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', @@ -515,6 +514,7 @@ test.serial('force merge a diverged project', async (t) => { // Change project.yaml const modified = myProject_yaml.replace('fn()', 'fn(x)'); + restoreFs(); mockFs({ '/ws/.projects': {}, '/ws/openfn.yaml': '', @@ -544,6 +544,7 @@ test.serial('force merge a diverged project', async (t) => { test.serial( 'fetch by UUID to an output path when local copies share the UUID', async (t) => { + restoreFs(); mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, @@ -570,6 +571,7 @@ test.serial( test.serial( 'error: fetch by UUID to an output path without an endpoint when local copies share the UUID', async (t) => { + restoreFs(); mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, @@ -596,6 +598,7 @@ test.serial( test.serial( 'error: fetch by UUID without an output path when local copies share the UUID', async (t) => { + restoreFs(); mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, @@ -620,6 +623,7 @@ test.serial( ); test.serial('fetch by alias when local copies share the UUID', async (t) => { + restoreFs(); mockFs({ '/ws/.projects/jam@app.openfn.org.yaml': yaml_v1, '/ws/.projects/jam-backup@app.openfn.org.yaml': yaml_v1, diff --git a/packages/cli/test/projects/list.test.ts b/packages/cli/test/projects/list.test.ts index 10fe029bb..d6d4da853 100644 --- a/packages/cli/test/projects/list.test.ts +++ b/packages/cli/test/projects/list.test.ts @@ -1,9 +1,11 @@ import test from 'ava'; -import { mockFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { handler as list } from '../../src/projects/list'; import { createMockLogger } from '@openfn/logger'; import { jsonToYaml } from '@openfn/project'; +test.afterEach(restoreFs); + // a workspace with several projects but no checkout metadata const noCheckoutProject = jsonToYaml({ id: 'my-project', workflows: [] }); @@ -136,11 +138,11 @@ files['/aliased/workflows/sw/simple-workflow.yaml'] = jsonToYaml({ steps: [], }); -mockFs(files); +test.beforeEach(() => mockFs(files)); const logger = createMockLogger('', { level: 'debug' }); -test('throw for invalid workspace directory', async (t) => { +test.serial('throw for invalid workspace directory', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/invalid' }, logger), { @@ -151,7 +153,7 @@ test('throw for invalid workspace directory', async (t) => { // t.is(message, 'Command was run in an invalid openfn workspace'); }); -test('throw if dir is not a workspace', async (t) => { +test.serial('throw if dir is not a workspace', async (t) => { await t.throwsAsync( () => list({ command: 'projects', workspace: '/no-ws' }, logger), { @@ -160,7 +162,7 @@ test('throw if dir is not a workspace', async (t) => { ); }); -test('valid workspace', async (t) => { +test.serial('valid workspace', async (t) => { await list({ command: 'projects', workspace: '/ws' }, logger); const { message } = logger._find('always', /available openfn projects/i); @@ -182,7 +184,7 @@ staging | my-project ); }); -test('shows workflow aliases on the active project', async (t) => { +test.serial('shows workflow aliases on the active project', async (t) => { const privateLogger = createMockLogger('', { level: 'debug' }); await list({ command: 'projects', workspace: '/aliased' }, privateLogger); diff --git a/packages/cli/test/projects/merge.test.ts b/packages/cli/test/projects/merge.test.ts index b5f5a691b..f90dd3fe1 100644 --- a/packages/cli/test/projects/merge.test.ts +++ b/packages/cli/test/projects/merge.test.ts @@ -1,10 +1,12 @@ import Project, { jsonToYaml, Workspace } from '@openfn/project'; import test from 'ava'; -import { mockFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { handler as mergeHandler } from '../../src/projects/merge'; import { createMockLogger } from '@openfn/logger'; import { handler as checkoutHandler } from '../../src/projects/checkout'; +test.afterEach(restoreFs); + const sandbox = { id: '', name: 'My Sandbox', @@ -216,6 +218,7 @@ test.serial( ); test.serial('Write to JSON using project config', async (t) => { + restoreFs(); mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, @@ -259,6 +262,7 @@ test.serial('Write to JSON using project config', async (t) => { }); test.serial('merge with custom base', async (t) => { + restoreFs(); mockFs({ '/ws/openfn.yaml': jsonToYaml({ dirs: { workflows: 'workflows' }, @@ -346,6 +350,7 @@ const multiMain = { }; const mockMultiWorkflowWorkspace = () => { + restoreFs(); mockFs({ '/ws/workflows': {}, '/ws/openfn.yaml': jsonToYaml({ diff --git a/packages/cli/test/projects/util.test.ts b/packages/cli/test/projects/util.test.ts index 0c598f75c..6e97957e6 100644 --- a/packages/cli/test/projects/util.test.ts +++ b/packages/cli/test/projects/util.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import Project, { generateWorkflow, yamlToJson } from '@openfn/project'; import { @@ -8,72 +8,91 @@ import { writeCheckoutFile, } from '../../src/projects/util'; -test.afterEach(() => { - restoreFs(); -}); +test.afterEach(restoreFs); -test('tidyWorkflowDir: removes workflows that no longer exist', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: removes workflows that no longer exist', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id D trigger-w'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id D trigger-w'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/B/B.yaml']); -}); + t.deepEqual(toRemove, ['workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: do nothing when no workflows are removed', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [generateWorkflow('@id A trigger-x')], - }); +test.serial( + 'tidyWorkflowDir: do nothing when no workflows are removed', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [generateWorkflow('@id A trigger-x')], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, []); -}); + t.deepEqual(toRemove, []); + } +); -test('tidyWorkflowDir: removes all workflows when incoming project is empty', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: removes all workflows when incoming project is empty', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - // All workflows should be removed - t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); -}); + // All workflows should be removed + t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: both projects empty', async (t) => { +test.serial('tidyWorkflowDir: both projects empty', async (t) => { const currentProject = new Project({ name: 'current', workflows: [], @@ -89,7 +108,7 @@ test('tidyWorkflowDir: both projects empty', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: identical projects', async (t) => { +test.serial('tidyWorkflowDir: identical projects', async (t) => { const currentProject = new Project({ name: 'current', workflows: [ @@ -111,29 +130,36 @@ test('tidyWorkflowDir: identical projects', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: complete replacement with no overlap', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [ - generateWorkflow('@id A trigger-x'), - generateWorkflow('@id B trigger-y'), - ], - }); +test.serial( + 'tidyWorkflowDir: complete replacement with no overlap', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [ + generateWorkflow('@id A trigger-x'), + generateWorkflow('@id B trigger-y'), + ], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [ - generateWorkflow('@id X trigger-x'), - generateWorkflow('@id Y trigger-y'), - ], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [ + generateWorkflow('@id X trigger-x'), + generateWorkflow('@id Y trigger-y'), + ], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); -}); + t.deepEqual(toRemove, ['workflows/A/A.yaml', 'workflows/B/B.yaml']); + } +); -test('tidyWorkflowDir: handles undefined projects', async (t) => { +test.serial('tidyWorkflowDir: handles undefined projects', async (t) => { const project = new Project({ name: 'project', workflows: [generateWorkflow('@id A trigger-x')], @@ -152,23 +178,30 @@ test('tidyWorkflowDir: handles undefined projects', async (t) => { t.deepEqual(toRemove, []); }); -test('tidyWorkflowDir: removes expression files when workflow steps change', async (t) => { - const currentProject = new Project({ - name: 'current', - workflows: [generateWorkflow('@id A trigger-x(expression=fn)')], - }); +test.serial( + 'tidyWorkflowDir: removes expression files when workflow steps change', + async (t) => { + const currentProject = new Project({ + name: 'current', + workflows: [generateWorkflow('@id A trigger-x(expression=fn)')], + }); - const incomingProject = new Project({ - name: 'incoming', - workflows: [generateWorkflow('@id A trigger-z(expression=fn)')], - }); + const incomingProject = new Project({ + name: 'incoming', + workflows: [generateWorkflow('@id A trigger-z(expression=fn)')], + }); - const toRemove = await tidyWorkflowDir(currentProject, incomingProject, true); + const toRemove = await tidyWorkflowDir( + currentProject, + incomingProject, + true + ); - t.deepEqual(toRemove, ['workflows/A/x.js']); -}); + t.deepEqual(toRemove, ['workflows/A/x.js']); + } +); -test('findLocallyChangedWorkflows: no changed workflows', async (t) => { +test.serial('findLocallyChangedWorkflows: no changed workflows', async (t) => { const wf1 = generateWorkflow('@id a trigger-x'); const wf2 = generateWorkflow('@id b trigger-y'); @@ -194,90 +227,102 @@ test('findLocallyChangedWorkflows: no changed workflows', async (t) => { t.deepEqual(changed, []); }); -test('findLocallyChangedWorkflows: all workflows changed if there is no forked_from', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); +test.serial( + 'findLocallyChangedWorkflows: all workflows changed if there is no forked_from', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); - // Create a mock workspace with NO forked_from - const workspace = { - activeProject: {}, - } as any; + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['a', 'b']); -}); + // Create a mock workspace with NO forked_from + const workspace = { + activeProject: {}, + } as any; -test('findLocallyChangedWorkflows: detect 1 locally changed workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-z'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['a', 'b']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), - b: wf2.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally changed workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-z'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + b: wf2.getVersionHash(), + }, }, - }, - } as any; - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); + } as any; - project.workflows[0].name = 'changed'; + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['a']); -}); + project.workflows[0].name = 'changed'; -test('findLocallyChangedWorkflows: detect 1 locally added workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['a']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally added workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + }, }, - }, - } as any; - - const project = new Project({ - name: 'test', - workflows: [wf1, wf2], - }); + } as any; - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['b']); -}); + const project = new Project({ + name: 'test', + workflows: [wf1, wf2], + }); -test('findLocallyChangedWorkflows: detect 1 locally removed workflow', async (t) => { - const wf1 = generateWorkflow('@id a trigger-x'); - const wf2 = generateWorkflow('@id b trigger-y'); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['b']); + } +); - const workspace = { - activeProject: { - forked_from: { - a: wf1.getVersionHash(), - b: wf2.getVersionHash(), +test.serial( + 'findLocallyChangedWorkflows: detect 1 locally removed workflow', + async (t) => { + const wf1 = generateWorkflow('@id a trigger-x'); + const wf2 = generateWorkflow('@id b trigger-y'); + + const workspace = { + activeProject: { + forked_from: { + a: wf1.getVersionHash(), + b: wf2.getVersionHash(), + }, }, - }, - } as any; + } as any; - const project = new Project({ - name: 'test', - workflows: [wf1], - }); + const project = new Project({ + name: 'test', + workflows: [wf1], + }); - const changed = await findLocallyChangedWorkflows(workspace, project); - t.deepEqual(changed, ['b']); -}); + const changed = await findLocallyChangedWorkflows(workspace, project); + t.deepEqual(changed, ['b']); + } +); const trackedProject = () => new Project({ diff --git a/packages/cli/test/pull/handler.test.ts b/packages/cli/test/pull/handler.test.ts index 95e35c9c0..909b7ab9d 100644 --- a/packages/cli/test/pull/handler.test.ts +++ b/packages/cli/test/pull/handler.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import fs from 'node:fs'; import { MockAgent, setGlobalDispatcher } from 'undici'; import { createMockLogger } from '@openfn/logger'; @@ -9,19 +9,15 @@ import { PullOptions } from '../../src/pull/command'; import { Workspace } from '@openfn/project'; import { myProject_v1 } from '../projects/fixtures'; +test.afterEach(restoreFs); + const ENDPOINT = 'https://fake.test.openfn.org'; const PROJECT_UUID = 'e16c5f09-f0cb-4ba7-a4c2-73fcb2f29d00'; test.beforeEach(() => { - restoreFs(); - process.env.OPENFN_API_KEY = 'fake-api-key'; }); -test.afterEach(() => { - restoreFs(); -}); - let mockAgent = new MockAgent(); mockAgent.disableNetConnect(); setGlobalDispatcher(mockAgent); diff --git a/packages/cli/test/util.ts b/packages/cli/test/util.ts index 93b2f581a..935bbd3c5 100644 --- a/packages/cli/test/util.ts +++ b/packages/cli/test/util.ts @@ -1,22 +1,8 @@ /* * test utils */ -import path from 'node:path'; -import { loadPath, mockFs as mockMemFs, restoreFs } from './helpers/memfs'; - import type { ExecutionPlan, Job, StepEdge } from '@openfn/runtime'; -export const mockFs = (files: Record) => { - mockMemFs({ - '/repo/': loadPath(path.resolve('test/__repo__/')), - ...files, - }); -}; - -export const resetMockFs = () => { - restoreFs(); -}; - type CreateWorkflowOptions = { id?: string; }; diff --git a/packages/cli/test/util/file-exists.test.ts b/packages/cli/test/util/file-exists.test.ts index 2aab35b43..2c9385189 100644 --- a/packages/cli/test/util/file-exists.test.ts +++ b/packages/cli/test/util/file-exists.test.ts @@ -1,22 +1,20 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { fileExists } from '../../src/util/file-exists'; -test.afterEach(() => { - restoreFs(); -}); +test.afterEach(restoreFs); -test('returns true for an existing file', async (t) => { +test.serial('returns true for an existing file', async (t) => { mockFs({ './test.txt': 'content' }); t.true(await fileExists('./test.txt')); }); -test('returns false for a non-existent path', async (t) => { +test.serial('returns false for a non-existent path', async (t) => { mockFs({}); t.false(await fileExists('./nonexistent.txt')); }); -test('returns false for a directory', async (t) => { +test.serial('returns false for a directory', async (t) => { mockFs({ './mydir': {} }); t.false(await fileExists('./mydir')); }); diff --git a/packages/cli/test/util/load-plan.test.ts b/packages/cli/test/util/load-plan.test.ts index 3531676dc..a7fcb76bf 100644 --- a/packages/cli/test/util/load-plan.test.ts +++ b/packages/cli/test/util/load-plan.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { omit } from 'lodash-es'; import type { Job } from '@openfn/runtime'; @@ -7,6 +7,8 @@ import type { Job } from '@openfn/runtime'; import loadPlan from '../../src/util/load-plan'; import { Opts } from '../../src/options'; +test.afterEach(restoreFs); + const logger = createMockLogger(undefined, { level: 'debug' }); const sampleXPlan = { @@ -42,7 +44,6 @@ test.beforeEach(() => { test.afterEach(() => { logger._reset(); - restoreFs(); }); // TODO: add some tests for handling yaml stuff @@ -205,6 +206,7 @@ test.serial('xplan: expand adaptors', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -235,6 +237,7 @@ test.serial('flat xplan: expand adaptors', async (t) => { }, ], }; + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -263,6 +266,7 @@ test.serial('xplan: do not expand adaptors', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -293,6 +297,7 @@ test.serial('xplan: set timeout from CLI', async (t) => { // @ts-ignore plan.options.timeout = 1; + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -318,6 +323,7 @@ test.serial('xplan: set start from CLI', async (t) => { // @ts-ignore plan.options.start = 'a'; + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -342,6 +348,7 @@ test.serial('xplan: map to monorepo', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), '/repo/packages/common': {}, @@ -388,6 +395,7 @@ test.serial('step: allow file paths for state', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/state.json': JSON.stringify({ data: { @@ -422,6 +430,7 @@ test.serial('xplan: support multiple adaptors', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -454,6 +463,7 @@ test.serial('xplan: append collections', async (t) => { }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -496,6 +506,7 @@ test.serial( }, ]); + restoreFs(); mockFs({ 'test/wf.json': JSON.stringify(plan), }); @@ -520,6 +531,7 @@ test.serial( test.serial( 'xplan: load a workflow.yaml without top workflow key', async (t) => { + restoreFs(); mockFs({ 'test/wf.yaml': ` start: a @@ -547,6 +559,7 @@ steps: test.serial( 'xplan: load a workflow.yaml without top workflow key and options', async (t) => { + restoreFs(); mockFs({ 'test/wf.yaml': ` name: wf @@ -574,6 +587,7 @@ options: ); test.serial('xplan: load a workflow.yaml with top workflow key', async (t) => { + restoreFs(); mockFs({ 'test/wf.yaml': ` workflow: @@ -597,6 +611,7 @@ options: }); test.serial('xplan: load a workflow through a Workspace', async (t) => { + restoreFs(); mockFs({ '/tmp/workflows/wf.yaml': ` id: wf @@ -629,6 +644,7 @@ dirs: }); test.serial('xplan: throw if a named workflow does not exist', async (t) => { + restoreFs(); mockFs({ '/tmp/workflows/wf.yaml': ` id: wf @@ -655,6 +671,7 @@ dirs: test.serial( 'xplan: load a workflow through a project .yaml and apply the credentials map by default', async (t) => { + restoreFs(); mockFs({ '/tmp/workflows/wf.yaml': ` id: wf diff --git a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts index d3a50992e..7c8f872e0 100644 --- a/packages/cli/test/util/map-adaptors-to-monorepo.test.ts +++ b/packages/cli/test/util/map-adaptors-to-monorepo.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import mapAdaptorsToMonorepo, { @@ -9,6 +9,8 @@ import mapAdaptorsToMonorepo, { } from '../../src/util/map-adaptors-to-monorepo'; import { ExecutionPlan } from '@openfn/runtime'; +test.afterEach(restoreFs); + // Paths are resolved to absolute in the option's ensure block, so the util // always receives absolute roots const REPO_PATH = path.resolve('a/b/c'); @@ -18,7 +20,6 @@ const logger = createMockLogger(); test.afterEach(() => { logger._reset(); - restoreFs(); }); test.serial('updatePath: common', (t) => { @@ -55,7 +56,7 @@ test.serial('updatePath: common@1.2.3 (with warning)', (t) => { t.regex(message as string, /ignoring version specifier/i); }); -test('updatePath: common=x/y/z', (t) => { +test.serial('updatePath: common=x/y/z', (t) => { const result = updatePath('common=x/y/z', [REPO_PATH], logger); t.is(result, `common=x/y/z`); diff --git a/packages/cli/test/util/print-versions.test.ts b/packages/cli/test/util/print-versions.test.ts index 841621cac..1a7a8dee3 100644 --- a/packages/cli/test/util/print-versions.test.ts +++ b/packages/cli/test/util/print-versions.test.ts @@ -1,12 +1,14 @@ import { createMockLogger, JSONLog } from '@openfn/logger'; import test from 'ava'; -import { loadPath, mockFs } from '../helpers/memfs'; +import { loadPath, mockFs, restoreFs } from '@openfn/test-helpers'; import path from 'node:path'; import printVersions from '../../src/util/print-versions'; +test.afterEach(restoreFs); + const root = path.resolve('package.json'); -test('print versions for node and cli', async (t) => { +test.serial('print versions for node and cli', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger); @@ -23,7 +25,7 @@ test('print versions for node and cli', async (t) => { t.notRegex(message, /runtime/); }); -test('print versions for node, cli and adaptor', async (t) => { +test.serial('print versions for node, cli and adaptor', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger, { adaptors: ['http'] }); @@ -36,35 +38,41 @@ test('print versions for node, cli and adaptor', async (t) => { t.regex(message, /http .+ latest/); }); -test('print versions for node, cli, components and adaptor', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['http'] }, true); - - const last = logger._parse(logger._last); - const message = last.message as string; - - t.regex(message, /Versions:/); - t.regex(message, /cli/); - t.regex(message, /node/); - t.regex(message, /runtime/); - t.regex(message, /node/); - t.regex(message, /http .+ latest/); -}); - -test('print versions for node, cli and adaptor with version', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['http@1234'] }); - - const last = logger._parse(logger._last); - const message = last.message as string; - - t.regex(message, /Versions:/); - t.regex(message, /cli/); - t.regex(message, /node/); - t.regex(message, /http .+ 1234/); -}); - -test('print versions for node, cli and long-form adaptor', async (t) => { +test.serial( + 'print versions for node, cli, components and adaptor', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['http'] }, true); + + const last = logger._parse(logger._last); + const message = last.message as string; + + t.regex(message, /Versions:/); + t.regex(message, /cli/); + t.regex(message, /node/); + t.regex(message, /runtime/); + t.regex(message, /node/); + t.regex(message, /http .+ latest/); + } +); + +test.serial( + 'print versions for node, cli and adaptor with version', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['http@1234'] }); + + const last = logger._parse(logger._last); + const message = last.message as string; + + t.regex(message, /Versions:/); + t.regex(message, /cli/); + t.regex(message, /node/); + t.regex(message, /http .+ 1234/); + } +); + +test.serial('print versions for node, cli and long-form adaptor', async (t) => { const logger = createMockLogger('', { level: 'info' }); await printVersions(logger, { adaptors: ['@openfn/language-http'] }); @@ -74,17 +82,20 @@ test('print versions for node, cli and long-form adaptor', async (t) => { t.regex(message, /@openfn\/language-http .+ latest/); }); -test('print versions for node, cli and long-form adaptor with version', async (t) => { - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { adaptors: ['@openfn/language-http@1234'] }); +test.serial( + 'print versions for node, cli and long-form adaptor with version', + async (t) => { + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { adaptors: ['@openfn/language-http@1234'] }); - const last = logger._parse(logger._last); - const message = last.message as string; + const last = logger._parse(logger._last); + const message = last.message as string; - t.regex(message, /@openfn\/language-http .+ 1234/); -}); + t.regex(message, /@openfn\/language-http .+ 1234/); + } +); -test('print version of adaptor with monorepo', async (t) => { +test.serial('print version of adaptor with monorepo', async (t) => { mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', [root]: loadPath(root), @@ -102,7 +113,7 @@ test('print version of adaptor with monorepo', async (t) => { t.regex(message, /@openfn\/language-http(.+)monorepo/); }); -test('print version of adaptor with path', async (t) => { +test.serial('print version of adaptor with path', async (t) => { mockFs({ '/repo/http/package.json': '{ "version": "1.0.0" }', [root]: loadPath(root), @@ -119,25 +130,28 @@ test('print version of adaptor with path', async (t) => { t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); }); -test('print version of adaptor with path even if monorepo is set', async (t) => { - mockFs({ - '/repo/http/package.json': '{ "version": "1.0.0" }', - [root]: loadPath(root), - }); +test.serial( + 'print version of adaptor with path even if monorepo is set', + async (t) => { + mockFs({ + '/repo/http/package.json': '{ "version": "1.0.0" }', + [root]: loadPath(root), + }); - const logger = createMockLogger('', { level: 'info' }); - await printVersions(logger, { - adaptors: ['@openfn/language-http=/repo/http'], - monorepoPath: ['.'], - }); + const logger = createMockLogger('', { level: 'info' }); + await printVersions(logger, { + adaptors: ['@openfn/language-http=/repo/http'], + monorepoPath: ['.'], + }); - const last = logger._parse(logger._last); - const message = last.message as string; + const last = logger._parse(logger._last); + const message = last.message as string; - t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); -}); + t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); + } +); -test('print version of adaptor with path and @', async (t) => { +test.serial('print version of adaptor with path and @', async (t) => { mockFs({ '/repo/node_modules/@openfn/http/package.json': '{ "version": "1.0.0" }', [root]: loadPath(root), @@ -154,7 +168,7 @@ test('print version of adaptor with path and @', async (t) => { t.regex(message, /@openfn\/language-http(.+)1\.0\.0/); }); -test('json output', async (t) => { +test.serial('json output', async (t) => { const logger = createMockLogger('', { level: 'info', json: true }); await printVersions(logger, { adaptors: ['http'], logJson: true }); diff --git a/packages/cli/test/util/validate-adaptors.test.ts b/packages/cli/test/util/validate-adaptors.test.ts index acfc4bb61..acd6c09bf 100644 --- a/packages/cli/test/util/validate-adaptors.test.ts +++ b/packages/cli/test/util/validate-adaptors.test.ts @@ -1,13 +1,11 @@ import { createMockLogger } from '@openfn/logger'; import test from 'ava'; -import { restoreFs } from '../helpers/memfs'; import validateAdaptors from '../../src/util/validate-adaptors'; const logger = createMockLogger('', { level: 'default' }); test.afterEach(() => { logger._reset(); - restoreFs(); }); test.serial('should warn if expression passed with no adaptor', async (t) => { diff --git a/packages/deploy/package.json b/packages/deploy/package.json index 9a392c640..36947f300 100644 --- a/packages/deploy/package.json +++ b/packages/deploy/package.json @@ -29,12 +29,12 @@ "license": "ISC", "devDependencies": { "@inquirer/testing": "^2.1.53", + "@openfn/test-helpers": "workspace:*", "@types/json-diff": "^1.0.3", "@types/jsonpath": "^0.2.4", "@types/node": "^18.19.130", "ava": "5.3.1", "jsonpath": "^1.3.0", - "memfs": "^4.80.0", "tslib": "^2.8.1", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/packages/deploy/test/helpers/memfs.ts b/packages/deploy/test/helpers/memfs.ts deleted file mode 100644 index 5f822da12..000000000 --- a/packages/deploy/test/helpers/memfs.ts +++ /dev/null @@ -1,119 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { syncBuiltinESMExports } from 'node:module'; -import { memfs, type NestedDirectoryJSON } from 'memfs'; - -/* - * Swaps node:fs for memfs while mocked. Every fs function is replaced once, - * when this module loads, with one that picks memfs or the real disk on each - * call. Libraries like glob and rimraf keep references to fs functions from - * load time, so import this before the code under test. - */ - -type FsObject = Record; - -// Tests run from the package folder, two below the repo root -const repoModules = path.resolve('../../node_modules') + path.sep; - -let mem: FsObject | undefined; -const realFds = new Set(); - -// The repo's own node_modules stay on the real disk, so that Node's module -// loader (which reads through node:fs) can still load dependencies lazily -const useReal = (target: unknown) => { - if (!mem) return true; - if (typeof target === 'number') return realFds.has(target); - if (typeof target === 'string') { - return path.resolve(target).startsWith(repoModules); - } - if (target instanceof URL) { - return path.resolve(fileURLToPath(target)).startsWith(repoModules); - } - return false; -}; - -const dispatch = ( - target: FsObject, - key: string, - getMem: () => FsObject | undefined -) => { - const real = target[key]; - const fn = (...args: any[]) => { - if (!useReal(args[0])) return getMem()![key](...args); - if (key === 'openSync') { - const fd = real(...args); - realFds.add(fd); - return fd; - } - if (key === 'open' && typeof args.at(-1) === 'function') { - const cb = args.pop(); - return real(...args, (err: Error | null, fd: number) => { - if (!err) realFds.add(fd); - cb(err, fd); - }); - } - if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); - return real(...args); - }; - if (real.native) { - fn.native = (...args: any[]) => - useReal(args[0]) ? real.native(...args) : getMem()![key](...args); - } - target[key] = fn; -}; - -// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream -// are left alone -const functionKeys = (obj: FsObject) => - Object.keys(obj).filter( - (key) => - /^[a-z]/.test(key) && - typeof obj[key] === 'function' && - Object.getOwnPropertyDescriptor(obj, key)?.writable - ); - -const realFs = fs as unknown as FsObject; -const realPromises = fs.promises as unknown as FsObject; -for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); -for (const key of functionKeys(realPromises)) { - dispatch(realPromises, key, () => mem?.promises); -} -syncBuiltinESMExports(); - -// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative -// paths in `tree` resolve against the cwd. -export const mockFs = (tree: NestedDirectoryJSON = {}) => { - restoreFs(); - const { fs: next } = memfs(tree, process.cwd()); - next.mkdirSync(process.cwd(), { recursive: true }); - next.mkdirSync(os.tmpdir(), { recursive: true }); - mem = next as unknown as FsObject; -}; - -export const restoreFs = () => { - mem = undefined; -}; - -const readReal = (target: string): NestedDirectoryJSON | Buffer => { - if (!fs.statSync(target).isDirectory()) { - return fs.readFileSync(target); - } - const dir: NestedDirectoryJSON = {}; - for (const name of fs.readdirSync(target)) { - dir[name] = readReal(path.join(target, name)); - } - return dir; -}; - -// Reads a real file or directory into a tree for mockFs, even while mocked -export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { - const current = mem; - mem = undefined; - try { - return readReal(target); - } finally { - mem = current; - } -}; diff --git a/packages/deploy/test/validator.test.ts b/packages/deploy/test/validator.test.ts index 03ee5843c..2b0e915ac 100644 --- a/packages/deploy/test/validator.test.ts +++ b/packages/deploy/test/validator.test.ts @@ -1,16 +1,14 @@ import test from 'ava'; -import { mockFs, restoreFs } from './helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { parseAndValidate } from '../src/validator'; +test.afterEach(restoreFs); + function findError(errors: any[], message: string) { return errors.find((e) => e.message === message); } -test.after(() => { - restoreFs(); -}); - -test('Workflows must be a map', async (t) => { +test.serial('Workflows must be a map', async (t) => { const doc = ` name: project-name workflows: @@ -26,7 +24,7 @@ test('Workflows must be a map', async (t) => { t.is(err.path, 'workflows'); }); -test('Workflows must have unique ids', async (t) => { +test.serial('Workflows must have unique ids', async (t) => { const doc = ` name: project-name workflows: @@ -45,7 +43,7 @@ test('Workflows must have unique ids', async (t) => { t.is(err.path, 'workflow-one'); }); -test('Jobs must have unique ids within a workflow', async (t) => { +test.serial('Jobs must have unique ids within a workflow', async (t) => { const doc = ` name: project-name workflows: @@ -64,7 +62,7 @@ test('Jobs must have unique ids within a workflow', async (t) => { t.truthy(err); }); -test('Job ids can duplicate across workflows', async (t) => { +test.serial('Job ids can duplicate across workflows', async (t) => { const doc = ` name: project-name workflows: @@ -83,7 +81,7 @@ test('Job ids can duplicate across workflows', async (t) => { t.is(results.errors.length, 0); }); -test('Workflow edges are parsed correctly', async (t) => { +test.serial('Workflow edges are parsed correctly', async (t) => { const doc = ` name: project-name workflows: @@ -115,7 +113,7 @@ test('Workflow edges are parsed correctly', async (t) => { ); }); -test('allow empty workflows', async (t) => { +test.serial('allow empty workflows', async (t) => { let doc = ` name: project-name `; @@ -130,7 +128,7 @@ test('allow empty workflows', async (t) => { }); }); -test('allows null channels by converting to empty map', async (t) => { +test.serial('allows null channels by converting to empty map', async (t) => { const doc = ` name: project-name channels: @@ -145,7 +143,7 @@ test('allows null channels by converting to empty map', async (t) => { t.deepEqual(result.doc.channels, {}); }); -test('parses channels with destination_credential', async (t) => { +test.serial('parses channels with destination_credential', async (t) => { const doc = ` name: project-name channels: @@ -170,15 +168,17 @@ test('parses channels with destination_credential', async (t) => { }); }); -test('adds the file content into the job body from the specified path', async (t) => { - // Step 1: Create a temporary file that the YAML will reference - const fileContent = 'fn(state => state.data);'; - mockFs({ - '/jobBody.js': fileContent, - }); - - // Step 2: YAML document that references the file - const doc = ` +test.serial( + 'adds the file content into the job body from the specified path', + async (t) => { + // Step 1: Create a temporary file that the YAML will reference + const fileContent = 'fn(state => state.data);'; + mockFs({ + '/jobBody.js': fileContent, + }); + + // Step 2: YAML document that references the file + const doc = ` name: project-name workflows: workflow-one: @@ -191,11 +191,12 @@ test('adds the file content into the job body from the specified path', async (t path: /jobBody.js `; - // Step 3: Run the parseAndValidate function - const results = await parseAndValidate(doc, 'spec.yaml'); + // Step 3: Run the parseAndValidate function + const results = await parseAndValidate(doc, 'spec.yaml'); - // Step 4: Assert that the content from the file was merged into the spec - const jobBody = results.doc.workflows['workflow-one'].jobs!['job-one'].body; + // Step 4: Assert that the content from the file was merged into the spec + const jobBody = results.doc.workflows['workflow-one'].jobs!['job-one'].body; - t.is(jobBody.content, fileContent); -}); + t.is(jobBody.content, fileContent); + } +); diff --git a/packages/project/package.json b/packages/project/package.json index a799cb28e..de6035384 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -27,9 +27,9 @@ "types": "dist/index.d.ts", "keywords": [], "devDependencies": { + "@openfn/test-helpers": "workspace:*", "@types/lodash": "^4.17.24", "ava": "5.3.1", - "memfs": "^4.80.0", "rimraf": "^6.1.3", "tslib": "^2.8.1", "tsup": "^8.5.1", diff --git a/packages/project/test/helpers/memfs.ts b/packages/project/test/helpers/memfs.ts deleted file mode 100644 index 5f822da12..000000000 --- a/packages/project/test/helpers/memfs.ts +++ /dev/null @@ -1,119 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { syncBuiltinESMExports } from 'node:module'; -import { memfs, type NestedDirectoryJSON } from 'memfs'; - -/* - * Swaps node:fs for memfs while mocked. Every fs function is replaced once, - * when this module loads, with one that picks memfs or the real disk on each - * call. Libraries like glob and rimraf keep references to fs functions from - * load time, so import this before the code under test. - */ - -type FsObject = Record; - -// Tests run from the package folder, two below the repo root -const repoModules = path.resolve('../../node_modules') + path.sep; - -let mem: FsObject | undefined; -const realFds = new Set(); - -// The repo's own node_modules stay on the real disk, so that Node's module -// loader (which reads through node:fs) can still load dependencies lazily -const useReal = (target: unknown) => { - if (!mem) return true; - if (typeof target === 'number') return realFds.has(target); - if (typeof target === 'string') { - return path.resolve(target).startsWith(repoModules); - } - if (target instanceof URL) { - return path.resolve(fileURLToPath(target)).startsWith(repoModules); - } - return false; -}; - -const dispatch = ( - target: FsObject, - key: string, - getMem: () => FsObject | undefined -) => { - const real = target[key]; - const fn = (...args: any[]) => { - if (!useReal(args[0])) return getMem()![key](...args); - if (key === 'openSync') { - const fd = real(...args); - realFds.add(fd); - return fd; - } - if (key === 'open' && typeof args.at(-1) === 'function') { - const cb = args.pop(); - return real(...args, (err: Error | null, fd: number) => { - if (!err) realFds.add(fd); - cb(err, fd); - }); - } - if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); - return real(...args); - }; - if (real.native) { - fn.native = (...args: any[]) => - useReal(args[0]) ? real.native(...args) : getMem()![key](...args); - } - target[key] = fn; -}; - -// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream -// are left alone -const functionKeys = (obj: FsObject) => - Object.keys(obj).filter( - (key) => - /^[a-z]/.test(key) && - typeof obj[key] === 'function' && - Object.getOwnPropertyDescriptor(obj, key)?.writable - ); - -const realFs = fs as unknown as FsObject; -const realPromises = fs.promises as unknown as FsObject; -for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); -for (const key of functionKeys(realPromises)) { - dispatch(realPromises, key, () => mem?.promises); -} -syncBuiltinESMExports(); - -// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative -// paths in `tree` resolve against the cwd. -export const mockFs = (tree: NestedDirectoryJSON = {}) => { - restoreFs(); - const { fs: next } = memfs(tree, process.cwd()); - next.mkdirSync(process.cwd(), { recursive: true }); - next.mkdirSync(os.tmpdir(), { recursive: true }); - mem = next as unknown as FsObject; -}; - -export const restoreFs = () => { - mem = undefined; -}; - -const readReal = (target: string): NestedDirectoryJSON | Buffer => { - if (!fs.statSync(target).isDirectory()) { - return fs.readFileSync(target); - } - const dir: NestedDirectoryJSON = {}; - for (const name of fs.readdirSync(target)) { - dir[name] = readReal(path.join(target, name)); - } - return dir; -}; - -// Reads a real file or directory into a tree for mockFs, even while mocked -export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { - const current = mem; - mem = undefined; - try { - return readReal(target); - } finally { - mem = current; - } -}; diff --git a/packages/project/test/parse/from-fs.test.ts b/packages/project/test/parse/from-fs.test.ts index 950f30a07..d69d000c1 100644 --- a/packages/project/test/parse/from-fs.test.ts +++ b/packages/project/test/parse/from-fs.test.ts @@ -1,12 +1,13 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { parseProject } from '../../src/parse/from-fs'; import { jsonToYaml, yamlToJson } from '../../src/util/yaml'; import { buildConfig } from '../../src/util/config'; +test.afterEach(restoreFs); + test.afterEach(() => { files = {}; - restoreFs(); }); let files: Record = {}; @@ -20,6 +21,7 @@ function mockFile(path: string, content: string | object) { // @ts-ignore files[path] = content; + restoreFs(); mockFs(files); } diff --git a/packages/project/test/parse/from-path.test.ts b/packages/project/test/parse/from-path.test.ts index f291d8d63..8d6b7b175 100644 --- a/packages/project/test/parse/from-path.test.ts +++ b/packages/project/test/parse/from-path.test.ts @@ -1,10 +1,12 @@ import test from 'ava'; -import { mockFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { generateProject } from '../../src'; import fromPath, { extractAliasFromFilename } from '../../src/parse/from-path'; import * as v2 from '../fixtures/sample-v2-project'; +test.afterEach(restoreFs); + const proj = generateProject('my-project', ['a-b'], { openfnUuid: true, }); @@ -105,27 +107,36 @@ test.serial('should apply a name override to a loaded project', async (t) => { t.is(project.name, 'My Duplicate'); }); -test('extractAliasFromFilename: should extract alias from alias@domain.yaml format', (t) => { - const alias = extractAliasFromFilename('main@app.openfn.org.yaml'); - t.is(alias, 'main'); -}); - -test('extractAliasFromFilename: should extract alias from alias@domain.json format', (t) => { - const alias = extractAliasFromFilename('staging@localhost.json'); - t.is(alias, 'staging'); -}); - -test('extractAliasFromFilename: should extract alias from simple filename', (t) => { - const alias = extractAliasFromFilename('production.yaml'); - t.is(alias, 'production'); -}); - -test('extractAliasFromFilename: should handle full paths', (t) => { +test.serial( + 'extractAliasFromFilename: should extract alias from alias@domain.yaml format', + (t) => { + const alias = extractAliasFromFilename('main@app.openfn.org.yaml'); + t.is(alias, 'main'); + } +); + +test.serial( + 'extractAliasFromFilename: should extract alias from alias@domain.json format', + (t) => { + const alias = extractAliasFromFilename('staging@localhost.json'); + t.is(alias, 'staging'); + } +); + +test.serial( + 'extractAliasFromFilename: should extract alias from simple filename', + (t) => { + const alias = extractAliasFromFilename('production.yaml'); + t.is(alias, 'production'); + } +); + +test.serial('extractAliasFromFilename: should handle full paths', (t) => { const alias = extractAliasFromFilename('/path/to/dev@app.openfn.org.yaml'); t.is(alias, 'dev'); }); -test('extractAliasFromFilename: should handle complex aliases', (t) => { +test.serial('extractAliasFromFilename: should handle complex aliases', (t) => { const alias = extractAliasFromFilename( 'my-project-staging@app.openfn.org.yaml' ); diff --git a/packages/project/test/util/config.test.ts b/packages/project/test/util/config.test.ts index 5ce334f46..ad2456531 100644 --- a/packages/project/test/util/config.test.ts +++ b/packages/project/test/util/config.test.ts @@ -1,5 +1,5 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { extractCheckout, extractConfig, @@ -10,11 +10,9 @@ import { } from '../../src/util/config'; import Project from '../../src'; -test.afterEach(() => { - restoreFs(); -}); +test.afterEach(restoreFs); -test('load config as yaml', (t) => { +test.serial('load config as yaml', (t) => { const yaml = ` workspace: dirs: @@ -65,7 +63,7 @@ project: test.todo('load config as json'); // Note that the legacy format is the old 0.6 version of openfn.yaml -test('legacy: load config as yaml', (t) => { +test.serial('legacy: load config as yaml', (t) => { const yaml = ` name: joe-sandbox-testing workflowRoot: workflows @@ -100,7 +98,7 @@ project: }); }); -test('legacy: load config as json', (t) => { +test.serial('legacy: load config as json', (t) => { const json = JSON.stringify({ name: 'joe-sandbox-testing', workflowRoot: 'workflows', @@ -137,7 +135,7 @@ test('legacy: load config as json', (t) => { }); }); -test('legacy: load empty config', (t) => { +test.serial('legacy: load empty config', (t) => { const yaml = ``; const result = loadWorkspaceFile(yaml); @@ -145,7 +143,7 @@ test('legacy: load empty config', (t) => { t.deepEqual(result.project, {}); }); -test('find openfn.yaml', (t) => { +test.serial('find openfn.yaml', (t) => { mockFs({ '/tmp/openfn.yaml': 'x: 1' }); const result = findWorkspaceFile('/tmp'); @@ -153,7 +151,7 @@ test('find openfn.yaml', (t) => { t.is(result.content, 'x: 1'); }); -test('find openfn.json', (t) => { +test.serial('find openfn.json', (t) => { mockFs({ '/tmp/openfn.json': '{ "x": 1 }' }); const result = findWorkspaceFile('/tmp'); @@ -161,7 +159,7 @@ test('find openfn.json', (t) => { t.deepEqual(result.content, { x: 1 }); }); -test('generate openfn.yaml (workspace config only)', (t) => { +test.serial('generate openfn.yaml (workspace config only)', (t) => { const proj = new Project( { id: 'my-project', @@ -196,7 +194,7 @@ formats: ); }); -test('generate openfn.yaml does not include collections', (t) => { +test.serial('generate openfn.yaml does not include collections', (t) => { const proj = new Project({ id: 'my-project', collections: [{ uuid: 'remote-uuid', name: 'my-collection' }], @@ -205,7 +203,7 @@ test('generate openfn.yaml does not include collections', (t) => { t.false(result.content.includes('collections')); }); -test('generate checkout file with forked_from', (t) => { +test.serial('generate checkout file with forked_from', (t) => { const proj = new Project({ id: 'my-project', name: 'My Project', @@ -229,7 +227,7 @@ uuid: 1234 ); }); -test('getCheckoutPath: default and per-branch', (t) => { +test.serial('getCheckoutPath: default and per-branch', (t) => { t.is(getCheckoutPath(), '.openfn/checkout.yaml'); t.is(getCheckoutPath(false), '.openfn/checkout.yaml'); t.is(getCheckoutPath('dev'), '.openfn/branches/dev/checkout.yaml'); @@ -239,7 +237,7 @@ test('getCheckoutPath: default and per-branch', (t) => { ); }); -test("checkout file excludes forked_from if it's not set", (t) => { +test.serial("checkout file excludes forked_from if it's not set", (t) => { const proj = new Project( { id: 'my-project', @@ -269,7 +267,7 @@ uuid: 1234 test.todo('generate openfn.json'); -test('include project name', (t) => { +test.serial('include project name', (t) => { const proj = new Project({ id: 'my-project', name: 'My Project', @@ -280,19 +278,22 @@ test('include project name', (t) => { test.todo('include parent project name'); -test('hasCheckoutMeta: false if there is no workspace at all', (t) => { +test.serial('hasCheckoutMeta: false if there is no workspace at all', (t) => { mockFs({ '/ws': {} }); t.false(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: false if there is a workspace but no checkout file', (t) => { - mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); +test.serial( + 'hasCheckoutMeta: false if there is a workspace but no checkout file', + (t) => { + mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml' }); - t.false(hasCheckoutMeta('/ws')); -}); + t.false(hasCheckoutMeta('/ws')); + } +); -test('hasCheckoutMeta: true if there is a checkout file', (t) => { +test.serial('hasCheckoutMeta: true if there is a checkout file', (t) => { mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': 'id: my-project', @@ -301,7 +302,7 @@ test('hasCheckoutMeta: true if there is a checkout file', (t) => { t.true(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: false if the checkout file is empty', (t) => { +test.serial('hasCheckoutMeta: false if the checkout file is empty', (t) => { mockFs({ '/ws/openfn.yaml': 'credentials: creds.yaml', '/ws/.openfn/checkout.yaml': '', @@ -310,23 +311,29 @@ test('hasCheckoutMeta: false if the checkout file is empty', (t) => { t.false(hasCheckoutMeta('/ws')); }); -test('hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', (t) => { - mockFs({ - '/ws/openfn.yaml': - 'project:\n id: my-project\nworkspace:\n credentials: creds.yaml', - }); - - t.true(hasCheckoutMeta('/ws')); -}); - -test('hasCheckoutMeta: looks for the checkout file for the given branch', (t) => { - mockFs({ - '/ws/openfn.yaml': 'credentials: creds.yaml', - '/ws/.openfn/branches/dev/checkout.yaml': 'id: my-project', - }); - - t.true(hasCheckoutMeta('/ws', 'dev')); - // other branches, and no branch, don't have any - t.false(hasCheckoutMeta('/ws', 'main')); - t.false(hasCheckoutMeta('/ws')); -}); +test.serial( + 'hasCheckoutMeta: true if there is a legacy project block in openfn.yaml', + (t) => { + mockFs({ + '/ws/openfn.yaml': + 'project:\n id: my-project\nworkspace:\n credentials: creds.yaml', + }); + + t.true(hasCheckoutMeta('/ws')); + } +); + +test.serial( + 'hasCheckoutMeta: looks for the checkout file for the given branch', + (t) => { + mockFs({ + '/ws/openfn.yaml': 'credentials: creds.yaml', + '/ws/.openfn/branches/dev/checkout.yaml': 'id: my-project', + }); + + t.true(hasCheckoutMeta('/ws', 'dev')); + // other branches, and no branch, don't have any + t.false(hasCheckoutMeta('/ws', 'main')); + t.false(hasCheckoutMeta('/ws')); + } +); diff --git a/packages/project/test/workspace.test.ts b/packages/project/test/workspace.test.ts index cee1696c9..1713dce3c 100644 --- a/packages/project/test/workspace.test.ts +++ b/packages/project/test/workspace.test.ts @@ -1,6 +1,8 @@ import Project, { jsonToYaml, Workspace } from '../src'; import test from 'ava'; -import { mockFs } from './helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; + +test.afterEach(restoreFs); const gen = (uuid: any, _alias: string, id: string, _domain: string) => jsonToYaml({ @@ -14,255 +16,257 @@ const gen = (uuid: any, _alias: string, id: string, _domain: string) => }); // TODO need a test on the legacy and new yaml formats here -mockFs({ - '/ws/openfn.yaml': jsonToYaml({ - project: { - id: 'project-1', - uuid: '1234', - }, - workspace: { - formats: { - openfn: 'yaml', - project: 'yaml', - workflow: 'yaml', - custom: true, // Note that this will be excluded +test.beforeEach(() => { + mockFs({ + '/ws/openfn.yaml': jsonToYaml({ + project: { + id: 'project-1', + uuid: '1234', }, - // deliberately exclude dirs - custom: true, - }, - }), - '/ws/workflows/wf-1/wf-1.yaml': jsonToYaml({ - id: 'wf-1', - steps: [{ id: 'a', expression: 'a.js' }], - }), - '/ws/workflows/wf-1/a.js': 'fn(s => s)', - '/ws/workflows/wf-2/wf-2.yaml': jsonToYaml({ - id: 'wf-2', - steps: [{ id: 'b', expression: 'b.js' }], - }), - '/ws/workflows/wf-2/b.js': 'fn(s => s)', - '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ - name: 'Project 1', - id: '', - workflows: [ - { - name: 'simple-workflow', // TODO clean up - id: 'wf-id', // TODO clean up - jobs: [ - { - name: 'Transform data to FHIR standard', - body: ' fn(state => state); // sdfl', - adaptor: '@openfn/language-http@latest', - id: 'job-a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'job-a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + workspace: { + formats: { + openfn: 'yaml', + project: 'yaml', + workflow: 'yaml', + custom: true, // Note that this will be excluded + }, + // deliberately exclude dirs + custom: true, }, - { - name: 'another-workflow', - id: 'another-id', - jobs: [ - { - name: 'Transform data to FHIR standard', - body: ' fn(state => state); // sdfl', - adaptor: '@openfn/language-http@latest', - id: 'job-b', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'job-b', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + }), + '/ws/workflows/wf-1/wf-1.yaml': jsonToYaml({ + id: 'wf-1', + steps: [{ id: 'a', expression: 'a.js' }], + }), + '/ws/workflows/wf-1/a.js': 'fn(s => s)', + '/ws/workflows/wf-2/wf-2.yaml': jsonToYaml({ + id: 'wf-2', + steps: [{ id: 'b', expression: 'b.js' }], + }), + '/ws/workflows/wf-2/b.js': 'fn(s => s)', + '/ws/.projects/staging@app.openfn.org.yaml': jsonToYaml({ + name: 'Project 1', + id: '', + workflows: [ + { + name: 'simple-workflow', // TODO clean up + id: 'wf-id', // TODO clean up + jobs: [ + { + name: 'Transform data to FHIR standard', + body: ' fn(state => state); // sdfl', + adaptor: '@openfn/language-http@latest', + id: 'job-a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'job-a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + { + name: 'another-workflow', + id: 'another-id', + jobs: [ + { + name: 'Transform data to FHIR standard', + body: ' fn(state => state); // sdfl', + adaptor: '@openfn/language-http@latest', + id: 'job-b', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'job-b', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // json format + '/ws2/openfn.yaml': jsonToYaml({ + project: { + id: 'project-2', + uuid: '', }, - ], - }), - - // json format - '/ws2/openfn.yaml': jsonToYaml({ - project: { - id: 'project-2', - uuid: '', - }, - workspace: { - formats: { - openfn: 'yaml', - project: 'json', - workflow: 'yaml', + workspace: { + formats: { + openfn: 'yaml', + project: 'json', + workflow: 'yaml', + }, }, - }, - }), - '/ws2/.projects/staging@app.openfn.org.json': JSON.stringify({ - name: 'Project 2', - id: '', - workflows: [ - { - name: 'Simple Workflow', - id: '', - jobs: [ - { - name: 'X', - body: '.', - adaptor: '@openfn/language-http@latest', - id: 'a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], - }, - ], - }), - - // custom paths - '/ws3/openfn.yaml': jsonToYaml({ - project: { - id: 'project-3', - uuid: '', - }, - workspace: { - dirs: { - projects: 'p', + }), + '/ws2/.projects/staging@app.openfn.org.json': JSON.stringify({ + name: 'Project 2', + id: '', + workflows: [ + { + name: 'Simple Workflow', + id: '', + jobs: [ + { + name: 'X', + body: '.', + adaptor: '@openfn/language-http@latest', + id: 'a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // custom paths + '/ws3/openfn.yaml': jsonToYaml({ + project: { + id: 'project-3', + uuid: '', }, - }, - }), - '/ws3/p/project@app.openfn.org.yaml': jsonToYaml({ - name: 'Project 3', - id: '', - workflows: [ - { - name: 'Simple Workflow', - id: '', - jobs: [ - { - name: 'X', - body: '.', - adaptor: '@openfn/language-http@latest', - id: 'a', - }, - ], - triggers: [ - { - type: 'webhook', - enabled: true, - id: 'trigger-id', - }, - ], - edges: [ - { - id: 'edge-id', - target_job_id: 'a', - enabled: true, - source_trigger_id: 'trigger-id', - condition_type: 'always', - }, - ], + workspace: { + dirs: { + projects: 'p', + }, }, - ], - }), - - // aliasing - // Two local copies of the same project, with "main" checked out - '/ws5/openfn.yaml': jsonToYaml({ - project: { id: 'proj-1', uuid: '111', alias: 'main' }, - }), - '/ws5/.projects/main@openfn.org.yaml': gen( - 111, - 'main', - 'proj-1', - 'openfn.org' - ), - '/ws5/.projects/backup@openfn.org.yaml': gen( - 111, - 'backup', - 'proj-1', - 'openfn.org' - ), - // Two copies on different ports of the same host - '/ws6/openfn.yaml': '', - '/ws6/.openfn/checkout.yaml': jsonToYaml({ - id: 'proj-1', - uuid: '222', - alias: 'local', - endpoint: 'http://localhost:5000', - }), - '/ws6/.projects/local@localhost.yaml': jsonToYaml({ - id: 'proj-1', - version: 2, - openfn: { uuid: '111', endpoint: 'http://localhost:4000' }, - workflows: [], - }), - '/ws6/.projects/other@localhost.yaml': jsonToYaml({ - id: 'proj-1', - version: 2, - openfn: { uuid: '222', endpoint: 'http://localhost:5000' }, - workflows: [], - }), - '/ws4/openfn.yaml': '', - '/ws4/.projects/main@openfn.org.yaml': gen( - 111, - 'main', - 'proj-1', - 'openfn.org' - ), - // prettier-ignore - '/ws4/.projects/main@somewhere.com.yaml': gen(112, 'main', 'proj-1', 'somewhere.com'), - // prettier-ignore - '/ws4/.projects/staging@openfn.org.yaml': gen(113, 'staging', 'proj-1-staging', 'openfn.org'), + }), + '/ws3/p/project@app.openfn.org.yaml': jsonToYaml({ + name: 'Project 3', + id: '', + workflows: [ + { + name: 'Simple Workflow', + id: '', + jobs: [ + { + name: 'X', + body: '.', + adaptor: '@openfn/language-http@latest', + id: 'a', + }, + ], + triggers: [ + { + type: 'webhook', + enabled: true, + id: 'trigger-id', + }, + ], + edges: [ + { + id: 'edge-id', + target_job_id: 'a', + enabled: true, + source_trigger_id: 'trigger-id', + condition_type: 'always', + }, + ], + }, + ], + }), + + // aliasing + // Two local copies of the same project, with "main" checked out + '/ws5/openfn.yaml': jsonToYaml({ + project: { id: 'proj-1', uuid: '111', alias: 'main' }, + }), + '/ws5/.projects/main@openfn.org.yaml': gen( + 111, + 'main', + 'proj-1', + 'openfn.org' + ), + '/ws5/.projects/backup@openfn.org.yaml': gen( + 111, + 'backup', + 'proj-1', + 'openfn.org' + ), + // Two copies on different ports of the same host + '/ws6/openfn.yaml': '', + '/ws6/.openfn/checkout.yaml': jsonToYaml({ + id: 'proj-1', + uuid: '222', + alias: 'local', + endpoint: 'http://localhost:5000', + }), + '/ws6/.projects/local@localhost.yaml': jsonToYaml({ + id: 'proj-1', + version: 2, + openfn: { uuid: '111', endpoint: 'http://localhost:4000' }, + workflows: [], + }), + '/ws6/.projects/other@localhost.yaml': jsonToYaml({ + id: 'proj-1', + version: 2, + openfn: { uuid: '222', endpoint: 'http://localhost:5000' }, + workflows: [], + }), + '/ws4/openfn.yaml': '', + '/ws4/.projects/main@openfn.org.yaml': gen( + 111, + 'main', + 'proj-1', + 'openfn.org' + ), + // prettier-ignore + '/ws4/.projects/main@somewhere.com.yaml': gen(112, 'main', 'proj-1', 'somewhere.com'), + // prettier-ignore + '/ws4/.projects/staging@openfn.org.yaml': gen(113, 'staging', 'proj-1-staging', 'openfn.org'), + }); }); -test('load the checked out project from the filesystem', async (t) => { +test.serial('load the checked out project from the filesystem', async (t) => { const project = await Project.from('fs', { root: '/ws' }); t.is(project.workflows.length, 2); }); -test('workspace-path: valid workspace path', (t) => { +test.serial('workspace-path: valid workspace path', (t) => { const ws = new Workspace('/ws'); t.is(ws.valid, true); t.truthy(ws.config); }); -test('workspace-path: invalid workspace path', (t) => { +test.serial('workspace-path: invalid workspace path', (t) => { const ws = new Workspace('/invalid'); t.is(ws.valid, false); @@ -272,7 +276,7 @@ test('workspace-path: invalid workspace path', (t) => { t.truthy(ws.config.formats); }); -test('workspace-list: list projects in the workspace', (t) => { +test.serial('workspace-list: list projects in the workspace', (t) => { const ws = new Workspace('/ws'); const result = ws.list(); @@ -283,7 +287,7 @@ test('workspace-list: list projects in the workspace', (t) => { ); }); -test('workspace-get: get projects in the workspace', (t) => { +test.serial('workspace-get: get projects in the workspace', (t) => { const ws = new Workspace('/ws'); const found = ws.get('project-1'); t.truthy(found); @@ -294,7 +298,7 @@ test('workspace-get: get projects in the workspace', (t) => { ); }); -test('load JSON files', (t) => { +test.serial('load JSON files', (t) => { const ws = new Workspace('/ws2'); const proj = ws.get('project-2'); t.truthy(proj); @@ -302,7 +306,7 @@ test('load JSON files', (t) => { t.is(proj?.workflows[0].id, 'simple-workflow'); }); -test('load from custom path', (t) => { +test.serial('load from custom path', (t) => { const ws = new Workspace('/ws3'); const proj = ws.get('project-3'); t.truthy(proj); @@ -310,7 +314,7 @@ test('load from custom path', (t) => { t.is(proj?.workflows[0].id, 'simple-workflow'); }); -test('load config', (t) => { +test.serial('load config', (t) => { const ws = new Workspace('/ws'); t.deepEqual(ws.config, { credentials: 'credentials.yaml', @@ -327,7 +331,7 @@ test('load config', (t) => { }); }); -test('load project meta', (t) => { +test.serial('load project meta', (t) => { const ws = new Workspace('/ws'); t.deepEqual(ws.activeProject, { uuid: '1234', @@ -335,14 +339,14 @@ test('load project meta', (t) => { }); }); -test('load v2 projects with multiple matching ids', (t) => { +test.serial('load v2 projects with multiple matching ids', (t) => { const ws = new Workspace('/ws4'); // @ts-ignore t.is(ws.projects.length, 3); }); -test('get project by id', (t) => { +test.serial('get project by id', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('proj-1-staging'); @@ -350,7 +354,7 @@ test('get project by id', (t) => { t.is(project?.id, 'proj-1-staging'); }); -test('get project by partial uuid', (t) => { +test.serial('get project by partial uuid', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('3'); @@ -358,14 +362,14 @@ test('get project by partial uuid', (t) => { t.is(project?.uuid, '113'); }); -test('get project returns null when not found', (t) => { +test.serial('get project returns null when not found', (t) => { const ws = new Workspace('/ws4'); const project = ws.get('non-existent'); t.is(project, null); }); -test('get project throws on ambiguous match', (t) => { +test.serial('get project throws on ambiguous match', (t) => { const ws = new Workspace('/ws4'); const error = t.throws(() => ws.get('main')); @@ -373,7 +377,7 @@ test('get project throws on ambiguous match', (t) => { t.regex(error!.message, /Failed to resolve unique identifier/); }); -test('get a duplicated project by uuid throws', (t) => { +test.serial('get a duplicated project by uuid throws', (t) => { const ws = new Workspace('/ws5'); t.throws(() => ws.get('111'), { @@ -381,19 +385,22 @@ test('get a duplicated project by uuid throws', (t) => { }); }); -test('getTrackedProject returns the checked out copy by alias', (t) => { +test.serial('getTrackedProject returns the checked out copy by alias', (t) => { const ws = new Workspace('/ws5'); t.is(ws.getTrackedProject()?.alias, 'main'); }); -test('getTrackedProject matches the alias on the same port only', (t) => { - const ws = new Workspace('/ws6'); +test.serial( + 'getTrackedProject matches the alias on the same port only', + (t) => { + const ws = new Workspace('/ws6'); - t.is(ws.getTrackedProject()?.alias, 'other'); -}); + t.is(ws.getTrackedProject()?.alias, 'other'); + } +); -test('getProjectPath returns the file for each copy', (t) => { +test.serial('getProjectPath returns the file for each copy', (t) => { const ws = new Workspace('/ws5'); t.is( diff --git a/packages/runtime/package.json b/packages/runtime/package.json index eaec45346..fc19887e0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -32,10 +32,10 @@ "@openfn/compiler": "workspace:^", "@openfn/language-common": "3.2.3", "@openfn/lexicon": "workspace:^", + "@openfn/test-helpers": "workspace:*", "@types/node": "^18.19.130", "@types/semver": "^7.7.1", "ava": "5.3.1", - "memfs": "^4.80.0", "recast": "^0.21.5", "ts-node": "^10.9.2", "tslib": "^2.8.1", diff --git a/packages/runtime/test/helpers/memfs.ts b/packages/runtime/test/helpers/memfs.ts deleted file mode 100644 index 5f822da12..000000000 --- a/packages/runtime/test/helpers/memfs.ts +++ /dev/null @@ -1,119 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { syncBuiltinESMExports } from 'node:module'; -import { memfs, type NestedDirectoryJSON } from 'memfs'; - -/* - * Swaps node:fs for memfs while mocked. Every fs function is replaced once, - * when this module loads, with one that picks memfs or the real disk on each - * call. Libraries like glob and rimraf keep references to fs functions from - * load time, so import this before the code under test. - */ - -type FsObject = Record; - -// Tests run from the package folder, two below the repo root -const repoModules = path.resolve('../../node_modules') + path.sep; - -let mem: FsObject | undefined; -const realFds = new Set(); - -// The repo's own node_modules stay on the real disk, so that Node's module -// loader (which reads through node:fs) can still load dependencies lazily -const useReal = (target: unknown) => { - if (!mem) return true; - if (typeof target === 'number') return realFds.has(target); - if (typeof target === 'string') { - return path.resolve(target).startsWith(repoModules); - } - if (target instanceof URL) { - return path.resolve(fileURLToPath(target)).startsWith(repoModules); - } - return false; -}; - -const dispatch = ( - target: FsObject, - key: string, - getMem: () => FsObject | undefined -) => { - const real = target[key]; - const fn = (...args: any[]) => { - if (!useReal(args[0])) return getMem()![key](...args); - if (key === 'openSync') { - const fd = real(...args); - realFds.add(fd); - return fd; - } - if (key === 'open' && typeof args.at(-1) === 'function') { - const cb = args.pop(); - return real(...args, (err: Error | null, fd: number) => { - if (!err) realFds.add(fd); - cb(err, fd); - }); - } - if (key === 'close' || key === 'closeSync') realFds.delete(args[0]); - return real(...args); - }; - if (real.native) { - fn.native = (...args: any[]) => - useReal(args[0]) ? real.native(...args) : getMem()![key](...args); - } - target[key] = fn; -}; - -// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream -// are left alone -const functionKeys = (obj: FsObject) => - Object.keys(obj).filter( - (key) => - /^[a-z]/.test(key) && - typeof obj[key] === 'function' && - Object.getOwnPropertyDescriptor(obj, key)?.writable - ); - -const realFs = fs as unknown as FsObject; -const realPromises = fs.promises as unknown as FsObject; -for (const key of functionKeys(realFs)) dispatch(realFs, key, () => mem); -for (const key of functionKeys(realPromises)) { - dispatch(realPromises, key, () => mem?.promises); -} -syncBuiltinESMExports(); - -// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative -// paths in `tree` resolve against the cwd. -export const mockFs = (tree: NestedDirectoryJSON = {}) => { - restoreFs(); - const { fs: next } = memfs(tree, process.cwd()); - next.mkdirSync(process.cwd(), { recursive: true }); - next.mkdirSync(os.tmpdir(), { recursive: true }); - mem = next as unknown as FsObject; -}; - -export const restoreFs = () => { - mem = undefined; -}; - -const readReal = (target: string): NestedDirectoryJSON | Buffer => { - if (!fs.statSync(target).isDirectory()) { - return fs.readFileSync(target); - } - const dir: NestedDirectoryJSON = {}; - for (const name of fs.readdirSync(target)) { - dir[name] = readReal(path.join(target, name)); - } - return dir; -}; - -// Reads a real file or directory into a tree for mockFs, even while mocked -export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { - const current = mem; - mem = undefined; - try { - return readReal(target); - } finally { - mem = current; - } -}; diff --git a/packages/runtime/test/repo/ensure-repo.test.ts b/packages/runtime/test/repo/ensure-repo.test.ts index 060e1ec3d..958335b05 100644 --- a/packages/runtime/test/repo/ensure-repo.test.ts +++ b/packages/runtime/test/repo/ensure-repo.test.ts @@ -1,12 +1,13 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { ensureRepo, loadRepoPkg } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger('test', { level: 'debug' }); test.afterEach(() => { - restoreFs(); logger._reset(); }); diff --git a/packages/runtime/test/repo/get-module-entry-point.test.ts b/packages/runtime/test/repo/get-module-entry-point.test.ts index 4f7aeb69b..ad9437186 100644 --- a/packages/runtime/test/repo/get-module-entry-point.test.ts +++ b/packages/runtime/test/repo/get-module-entry-point.test.ts @@ -1,17 +1,17 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; -import path from 'node:path'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { getModuleEntryPoint } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger(); test.afterEach(() => { - restoreFs(); logger._reset(); }); -const repoPath = path.resolve('test/__repo__'); +const repoPath = '/repo'; const mockRepo = (pkg: object) => { mockFs({ diff --git a/packages/runtime/test/repo/get-module-path.test.ts b/packages/runtime/test/repo/get-module-path.test.ts index 4ec37f976..abd4a7001 100644 --- a/packages/runtime/test/repo/get-module-path.test.ts +++ b/packages/runtime/test/repo/get-module-path.test.ts @@ -1,12 +1,13 @@ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { getModulePath } from '../../src/modules/repo'; +test.afterEach(restoreFs); + const logger = createMockLogger(); test.afterEach(() => { - restoreFs(); logger._reset(); }); diff --git a/packages/runtime/test/repo/install.test.ts b/packages/runtime/test/repo/install.test.ts index d6af38278..694daec86 100644 --- a/packages/runtime/test/repo/install.test.ts +++ b/packages/runtime/test/repo/install.test.ts @@ -3,15 +3,16 @@ * Testing with actual installs is left as an exercise for integration testing */ import test from 'ava'; -import { mockFs, restoreFs } from '../helpers/memfs'; +import { mockFs, restoreFs } from '@openfn/test-helpers'; import { createMockLogger } from '@openfn/logger'; import { install, loadRepoPkg } from '../../src'; import exec from '../../src/util/exec'; +test.afterEach(restoreFs); + const mockLogger = createMockLogger(undefined, { level: 'debug' }); test.afterEach(() => { - restoreFs(); mockLogger._reset(); }); @@ -139,6 +140,7 @@ test.serial('install with the correct alias', async (t) => { test.serial('do nothing if the package is already installed', async (t) => { const exec = mockExec(); + restoreFs(); mockFs({ '/tmp/repo/package.json': JSON.stringify({ name: 'test', diff --git a/packages/test-helpers/package.json b/packages/test-helpers/package.json new file mode 100644 index 000000000..679b622ce --- /dev/null +++ b/packages/test-helpers/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openfn/test-helpers", + "version": "0.0.0", + "private": true, + "description": "Shared helpers for kit's unit tests", + "type": "module", + "main": "src/memfs.ts", + "types": "src/memfs.ts", + "license": "ISC", + "dependencies": { + "memfs": "^4.80.0" + } +} diff --git a/packages/test-helpers/src/memfs.ts b/packages/test-helpers/src/memfs.ts new file mode 100644 index 000000000..ac655a323 --- /dev/null +++ b/packages/test-helpers/src/memfs.ts @@ -0,0 +1,181 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import { fileURLToPath } from 'node:url'; +import { syncBuiltinESMExports } from 'node:module'; +import { memfs, type NestedDirectoryJSON } from 'memfs'; + +/* + * Swaps node:fs for memfs while mocked. Every fs function is replaced once, + * when this module loads, with one that picks memfs or the real disk on each + * call. Libraries like glob and rimraf keep references to fs functions from + * load time, so the root ava config preloads this before any test file. + */ + +type FsObject = Record; + +const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)); + +let mem: FsObject | undefined; +const memFds = new Set(); + +const toPath = (target: unknown) => { + if (typeof target === 'string') return target; + if (Buffer.isBuffer(target)) return target.toString(); + if (target instanceof URL) return fileURLToPath(target); +}; + +// node_modules folders in the repo stay on the real disk, so that Node's +// module loader (which reads through node:fs) can still load dependencies +// lazily. Mocked trees can still hold their own node_modules elsewhere. +const useReal = (target: unknown) => { + if (!mem) return true; + if (typeof target === 'number') return !memFds.has(target); + const p = toPath(target); + if (p === undefined) return false; + const abs = path.resolve(p); + return ( + abs.startsWith(repoRoot) && + abs.slice(repoRoot.length).split(path.sep).includes('node_modules') + ); +}; + +const exdev = (syscall: string, from: unknown, to: unknown) => + Object.assign( + new Error( + `EXDEV: cross-device link not permitted, ${syscall} '${from}' -> '${to}'` + ), + { code: 'EXDEV', syscall } + ); + +// Two-path calls go to the destination's side. A copyFile across sides +// copies the bytes; anything else across sides fails like a cross-device move. +const twoPath = new Set(['copyFile', 'cp', 'rename', 'link', 'symlink']); + +const crossSides = (name: string, [src, dest, mode]: any[]) => { + if (name !== 'copyFile') throw exdev(name, src, dest); + const excl = typeof mode === 'number' && mode & fs.constants.COPYFILE_EXCL; + fs.writeFileSync(dest, fs.readFileSync(src), { flag: excl ? 'wx' : 'w' }); +}; + +const dispatch = ( + target: FsObject, + key: string, + getMem: () => FsObject | undefined, + isPromise: boolean +) => { + const real = target[key]; + const name = key.replace(/Sync$/, ''); + const sync = key.endsWith('Sync'); + + const fn: any = (...args: any[]) => { + if (!mem) return real(...args); + if (twoPath.has(name)) { + const destReal = useReal(args[1]); + if (name !== 'symlink' && useReal(args[0]) !== destReal) { + if (sync) return crossSides(name, args); + const done = new Promise((resolve) => + resolve(crossSides(name, args)) + ); + if (isPromise) return done; + const cb = args.at(-1); + done.then(() => cb(null), cb); + return; + } + return destReal ? real(...args) : getMem()![key](...args); + } + if (useReal(args[0])) return real(...args); + + const memFn = getMem()![key]; + if (isPromise && key === 'open') { + return memFn(...args).then((handle: any) => { + memFds.add(handle.fd); + return handle; + }); + } + if (key === 'open' && typeof args.at(-1) === 'function') { + const cb = args.pop(); + return memFn(...args, (err: Error | null, fd: number) => { + if (!err) memFds.add(fd); + cb(err, fd); + }); + } + const result = memFn(...args); + if (key === 'openSync') memFds.add(result); + if (name === 'close') memFds.delete(args[0]); + return result; + }; + + // Keeps util.promisify working: customPromisifyArgs on read and write, + // and promisify.custom on exists (whose callback takes no error) + for (const sym of Object.getOwnPropertySymbols(real)) fn[sym] = real[sym]; + if (key === 'exists') { + fn[promisify.custom] = (p: unknown) => + new Promise((resolve) => fn(p, resolve)); + } + if (real.native) { + fn.native = (...args: any[]) => + useReal(args[0]) ? real.native(...args) : getMem()![key](...args); + } + target[key] = fn; +}; + +// Classes like fs.ReadStream and getter-only exports like fs.Utf8Stream +// are left alone +const functionKeys = (obj: FsObject) => + Object.keys(obj).filter( + (key) => + /^[a-z]/.test(key) && + typeof obj[key] === 'function' && + Object.getOwnPropertyDescriptor(obj, key)?.writable + ); + +const realFs = fs as unknown as FsObject; +const realPromises = fs.promises as unknown as FsObject; +for (const key of functionKeys(realFs)) { + dispatch(realFs, key, () => mem, false); +} +for (const key of functionKeys(realPromises)) { + dispatch(realPromises, key, () => mem?.promises, true); +} +syncBuiltinESMExports(); + +// Mocks the filesystem with only `tree`, plus the cwd and tmpdir. Relative +// paths in `tree` resolve against the cwd. +export const mockFs = (tree: NestedDirectoryJSON = {}) => { + if (mem) { + throw new Error('mockFs called while already mocked: call restoreFs first'); + } + const { fs: next } = memfs(tree, process.cwd()); + next.mkdirSync(process.cwd(), { recursive: true }); + next.mkdirSync(os.tmpdir(), { recursive: true }); + memFds.clear(); + mem = next as unknown as FsObject; +}; + +export const restoreFs = () => { + mem = undefined; +}; + +const readReal = (target: string): NestedDirectoryJSON | Buffer => { + if (!fs.statSync(target).isDirectory()) { + return fs.readFileSync(target); + } + const dir: NestedDirectoryJSON = {}; + for (const name of fs.readdirSync(target)) { + dir[name] = readReal(path.join(target, name)); + } + return dir; +}; + +// Reads a real file or directory into a tree for mockFs, even while mocked +export const loadPath = (target: string): NestedDirectoryJSON | Buffer => { + const current = mem; + mem = undefined; + try { + return readReal(target); + } finally { + mem = current; + } +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8de1addf5..fdcf6ff6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -411,6 +411,9 @@ importers: '@openfn/language-common': specifier: 3.2.3 version: 3.2.3 + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/json-diff': specifier: ^1.0.3 version: 1.0.3 @@ -438,9 +441,6 @@ importers: lodash-es: specifier: ^4.17.23 version: 4.17.23 - memfs: - specifier: ^4.80.0 - version: 4.80.0 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -515,6 +515,9 @@ importers: '@inquirer/testing': specifier: ^2.1.53 version: 2.1.53(@types/node@18.19.130) + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/json-diff': specifier: ^1.0.3 version: 1.0.3 @@ -530,9 +533,6 @@ importers: jsonpath: specifier: ^1.3.0 version: 1.3.0 - memfs: - specifier: ^4.80.0 - version: 4.80.0 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -800,15 +800,15 @@ importers: specifier: ^2.8.2 version: 2.8.2 devDependencies: + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/lodash': specifier: ^4.17.24 version: 4.17.24 ava: specifier: 5.3.1 version: 5.3.1(supports-color@7.2.0) - memfs: - specifier: ^4.80.0 - version: 4.80.0 rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -849,6 +849,9 @@ importers: '@openfn/lexicon': specifier: workspace:^ version: link:../lexicon + '@openfn/test-helpers': + specifier: workspace:* + version: link:../test-helpers '@types/node': specifier: ^18.19.130 version: 18.19.130 @@ -858,9 +861,6 @@ importers: ava: specifier: 5.3.1 version: 5.3.1(supports-color@7.2.0) - memfs: - specifier: ^4.80.0 - version: 4.80.0 recast: specifier: ^0.21.5 version: 0.21.5 @@ -877,6 +877,12 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/test-helpers: + dependencies: + memfs: + specifier: ^4.80.0 + version: 4.80.0 + packages/ws-worker: dependencies: '@koa/bodyparser': From 5abbc1b60582a3109989770e42722bfc53506f36 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Tue, 20 Oct 2026 18:35:32 +0200 Subject: [PATCH 11/12] Fix the env overrides test's expected count --- packages/cli/test/env.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/env.test.ts b/packages/cli/test/env.test.ts index 7af97e184..d2f898900 100644 --- a/packages/cli/test/env.test.ts +++ b/packages/cli/test/env.test.ts @@ -70,10 +70,13 @@ test.serial('should use $DOT_ENV_OVERRIDES found', (t) => { // don't load .env // but set this magic env var instead process.env.$DOT_ENV_OVERRIDES = 'FOO,BAR'; + t.teardown(() => { + delete process.env.$DOT_ENV_OVERRIDES; + }); report(logger); - const a = logger._find('always', /imported 1 env vars/i); + const a = logger._find('always', /imported 2 env vars/i); t.truthy(a); const b = logger._find('debug', /set from .env:/i); From 997eeba1a48ca2f2466f8f770d7c85c8644ff824 Mon Sep 17 00:00:00 2001 From: Stuart Corbishley Date: Wed, 21 Oct 2026 08:42:22 +0200 Subject: [PATCH 12/12] Make ws-worker's Sentry tests wait for their own report waitForSentryReport takes a predicate and throws SENTRY_REPORT_TIMEOUT when nothing matches, so a capture from an earlier test that lands late can no longer be mistaken for the report a test expects. The Sentry changeset now notes that repeated identical errors are deduplicated. --- .changeset/worker-sentry-11.md | 2 +- packages/ws-worker/test/api/execute.test.ts | 24 ++++-- packages/ws-worker/test/util.ts | 20 ++--- .../ws-worker/test/util/send-event.test.ts | 79 ++++++++++++------- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/.changeset/worker-sentry-11.md b/.changeset/worker-sentry-11.md index a27dfb75c..11edb0d48 100644 --- a/.changeset/worker-sentry-11.md +++ b/.changeset/worker-sentry-11.md @@ -2,4 +2,4 @@ '@openfn/ws-worker': patch --- -Upgrade Sentry to v11 to stop a Node deprecation warning at startup +Upgrade Sentry to v11 to stop a Node deprecation warning at startup, and deduplicate repeated identical errors diff --git a/packages/ws-worker/test/api/execute.test.ts b/packages/ws-worker/test/api/execute.test.ts index dbce02066..7210a570a 100644 --- a/packages/ws-worker/test/api/execute.test.ts +++ b/packages/ws-worker/test/api/execute.test.ts @@ -72,6 +72,12 @@ test('send event should throw if an event errors', async (t) => { await t.throwsAsync(() => sendEvent(context, 'throw', 22), { message: '[throw] Error: err', }); + + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'throw' + ); + t.is(report.error.message, '[throw] Error: err'); }); test('jobLog should send a log event outside a run', async (t) => { @@ -300,12 +306,12 @@ test('loadDataclip report to sentry on fail', async (t) => { await loadDataclip(context, 'xyz'); } catch (e) {} - const reports = await waitForSentryReport(testkit); - const dataclip_report = reports.find((r: any) => - /fetch\:dataclip.+not_found/i.test(r.error.message) + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === GET_DATACLIP ); - t.truthy(dataclip_report); - t.is(dataclip_report.error.name, 'LightningSocketError'); + t.regex(report.error.message, /fetch\:dataclip.+not_found/i); + t.is(report.error.name, 'LightningSocketError'); }); test('loadCredential should fetch a credential', async (t) => { @@ -333,9 +339,11 @@ test('loadCredential report to sentry on fail', async (t) => { await loadCredential(context, 'abc'); } catch (e) {} - const reports = await waitForSentryReport(testkit); - t.is(reports.length, 1); - t.is(reports[0].error.name, 'LightningSocketError'); + const isCredentialReport = (r: any) => + r.tags.lightning_event === GET_CREDENTIAL; + const report = await waitForSentryReport(testkit, isCredentialReport); + t.is(testkit.reports().filter(isCredentialReport).length, 1); + t.is(report.error.name, 'LightningSocketError'); }); test('execute should pass the final result to onFinish', async (t) => { diff --git a/packages/ws-worker/test/util.ts b/packages/ws-worker/test/util.ts index 24916d931..7f933a572 100644 --- a/packages/ws-worker/test/util.ts +++ b/packages/ws-worker/test/util.ts @@ -20,19 +20,21 @@ export const initSentry = () => { return testkit; }; -// Telemetry will asynchronously submit the report in the background -// We have to wait for it here in this nasty loop :( -export const waitForSentryReport = async (testkit: any, max = 500) => { +// Sentry submits reports in the background, and a capture from an earlier +// test can land after the testkit is reset, so poll for the report you expect +export const waitForSentryReport = async ( + testkit: any, + match: (report: any) => boolean, + max = 500 +) => { for (let i = 0; i < max; i++) { - if (i >= max) { - throw new Error('SENTRY_REPORT_TIMEOUT'); - } - const reports = testkit.reports(); - if (reports.length) { - return reports; + const report = testkit.reports().find(match); + if (report) { + return report; } await sleep(1); } + throw new Error('SENTRY_REPORT_TIMEOUT'); }; export const wait = (fn: () => any, maxRuns = 100) => diff --git a/packages/ws-worker/test/util/send-event.test.ts b/packages/ws-worker/test/util/send-event.test.ts index 737e319ee..c9168d6bb 100644 --- a/packages/ws-worker/test/util/send-event.test.ts +++ b/packages/ws-worker/test/util/send-event.test.ts @@ -250,7 +250,7 @@ test.serial('should log if the event is rejected', async (t) => { }); test.serial('should report to sentry if the event is rejected', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'rejected'; const channel = mockChannel({ [EVENT_NAME]: () => { throw new Error('err'); @@ -273,12 +273,15 @@ test.serial('should report to sentry if the event is rejected', async (t) => { t.true(e.reportedToSentry); } - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningSocketError'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.error?.name, 'LightningSocketError'); }); test.serial('should report to sentry if the event timesout', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'timesout'; const channel = mockChannel({}); const context = { @@ -296,13 +299,16 @@ test.serial('should report to sentry if the event timesout', async (t) => { } catch (e: any) { t.true(e.reportedToSentry); } - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningTimeoutError'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.error?.name, 'LightningTimeoutError'); // Tags are indexed (unlike the run context below), so these are what make // it possible to ask sentry "is it only step:complete that times out?" - t.is(reports[0].tags.run_id, 'x'); - t.is(reports[0].tags.lightning_event, EVENT_NAME); + t.is(report.tags.run_id, 'x'); + t.is(report.tags.lightning_event, EVENT_NAME); }); test.serial( @@ -320,9 +326,12 @@ test.serial( sendEvent({ id: 'x', channel, logger, options: {} }, 'step:complete', {}) ); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].extra?.channel_state, 'errored'); - t.is(reports[0].extra?.socket_state, 'connecting'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'step:complete' + ); + t.is(report.extra?.channel_state, 'errored'); + t.is(report.extra?.socket_state, 'connecting'); } ); @@ -337,14 +346,17 @@ test.serial('should report to sentry against the run scope', async (t) => { await t.throwsAsync(() => sendEvent(context, 'step:complete', {})); stop(); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].error?.name, 'LightningSocketError'); - t.is(reports[0].tags.run_id, 'run-1'); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.run_id === 'run-1' + ); + t.is(report.error?.name, 'LightningSocketError'); + t.is(report.tags.run_id, 'run-1'); // The run's breadcrumb trail must survive too - this is why the capture // re-enters the scope rather than passing it to captureException, which // merges tags but drops breadcrumbs - const trail = reports[0].originalReport?.breadcrumbs ?? []; + const trail = report.originalReport?.breadcrumbs ?? []; t.true(trail.some((b: any) => b.message === 'job-complete')); }); @@ -353,10 +365,7 @@ test.serial( async (t) => { // Without this, every timeout for every event collapses into one sentry // issue - this is the change that would have made the step:complete - // pattern visible without digging through raw events. Each event is - // checked against a fresh testkit so the two reports cannot be confused - // with each other or raced against waitForSentryReport's "at least one" - // polling. + // pattern visible without digging through raw events. const channelA = mockChannel({}); await t.throwsAsync(() => sendEvent( @@ -365,14 +374,15 @@ test.serial( {} ) ); - const [stepReport] = await waitForSentryReport(testkit); + const stepReport = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'step:complete' + ); t.deepEqual(stepReport.originalReport.fingerprint, [ 'LightningTimeoutError', 'step:complete', ]); - testkit.reset(); - const channelB = mockChannel({}); await t.throwsAsync(() => sendEvent( @@ -381,7 +391,10 @@ test.serial( {} ) ); - const [runReport] = await waitForSentryReport(testkit); + const runReport = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === 'run:complete' + ); t.deepEqual(runReport.originalReport.fingerprint, [ 'LightningTimeoutError', 'run:complete', @@ -392,7 +405,7 @@ test.serial( test.serial( 'should report caller-supplied sentryExtras alongside a failed event', async (t) => { - const EVENT_NAME = 'test'; + const EVENT_NAME = 'extras'; const channel = { ...mockChannel({}), state: 'joined' }; const context = { id: 'x', channel, logger, options: {} }; @@ -406,9 +419,21 @@ test.serial( ) ); - const reports = await waitForSentryReport(testkit); - t.is(reports[0].extra?.payloadSize_b, 1536); + const report = await waitForSentryReport( + testkit, + (r: any) => r.tags.lightning_event === EVENT_NAME + ); + t.is(report.extra?.payloadSize_b, 1536); // sentryExtras must not crowd out the fields send-event already reports - t.is(reports[0].extra?.channel_state, 'joined'); + t.is(report.extra?.channel_state, 'joined'); + } +); + +test.serial( + 'waitForSentryReport should throw if no report matches in time', + async (t) => { + await t.throwsAsync(() => waitForSentryReport(testkit, () => false, 5), { + message: 'SENTRY_REPORT_TIMEOUT', + }); } );