From db1d75d138274195f64808e35569cea36e13af59 Mon Sep 17 00:00:00 2001 From: finalerock44 <77282157+finalerock44@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:54:49 +0100 Subject: [PATCH] chore: release 5.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote from dev to stable: * fix(cloud): exclude config-shaped files from flow discovery (#114, closes dcd-cli#99) — a `config.yaml` sitting in a flows folder was picked up as a flow and blew up `processDependencies` with "Expected an array of steps". Detection is by shape, not filename, so several named configs can coexist in one folder. A flow merely *missing* its `---` separator still errors loudly rather than being silently dropped. * fix(cloud): reject malformed executionOrder instead of silently running in parallel (#117, closes dcd-cli#110) — the config was `yaml.load`ed and straight-cast, so an `executionOrder` in the wrong shape was ignored and every flow ran in parallel. A zod schema (`src/services/workspace-config.schema.ts`) is now the single source of truth, with `IWorkspaceConfig` inferred from it so the compile-time and runtime views cannot drift. * deps: bump the minor-and-patch group with 5 updates (#112), plus eslint-plugin-unicorn 72 -> 73 and pnpm/action-setup 6.0.9 -> 6.0.10. Behaviour changes users will notice: * A malformed `executionOrder` is now a hard error with a targeted message showing found-vs-expected. Anyone whose config was mis-shaped has been running flows in parallel without knowing; after this they get a clear failure instead. That is the point of the fix, but it is the one change that can turn a previously-green pipeline red. * Unrecognised top-level config keys emit a warning (with a did-you-mean for near-misses). Keys are preserved, not stripped — the config is forwarded to the API as `fields.workspaceConfig`, so stripping would silently alter the payload. * Config warnings go to stderr, so `--json` stdout stays parseable and the MCP server's JSON-RPC stdout channel stays clean. No platform prerequisite: both fixes are client-side (flow discovery and config validation). The submitted payload for an already-valid config is unchanged. Carries only the source delta — package.json version, CHANGELOG.md and the release-please manifests stay as release-please left them on production. Release-As: 5.3.1 --- .github/workflows/cli-ci.yml | 2 +- .github/workflows/npm-publish.yml | 2 +- .github/workflows/release-binaries.yml | 2 +- package.json | 2 +- pnpm-lock.yaml | 508 +++++++++++---------- src/commands/cloud.ts | 3 + src/mcp/tools/run-cloud-test.ts | 2 + src/services/execution-plan.service.ts | 171 +++---- src/services/execution-plan.utils.ts | 72 ++- src/services/workspace-config.schema.ts | 255 +++++++++++ test/fixtures/basic-config.yaml | 9 +- test/fixtures/tag-filtering-config.yaml | 11 +- test/integration/cloud.integration.test.ts | 205 +++++++++ 13 files changed, 887 insertions(+), 357 deletions(-) create mode 100644 src/services/workspace-config.schema.ts diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index abee162..0af984a 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -77,7 +77,7 @@ jobs: /api/swagger.json - name: Setup pnpm - uses: pnpm/action-setup@v6.0.9 + uses: pnpm/action-setup@v6.0.10 with: version: 10 run_install: false diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 17873d4..4611165 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v7 # Setup .npmrc file to publish to npm - name: Setup pnpm - uses: pnpm/action-setup@v6.0.9 + uses: pnpm/action-setup@v6.0.10 with: run_install: false diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index cb4b85c..219d6fc 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v6.0.9 + uses: pnpm/action-setup@v6.0.10 with: run_install: false diff --git a/package.json b/package.json index d34cb2c..bf2b7b1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "chai": "^6.2.2", "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-unicorn": "^72.0.0", + "eslint-plugin-unicorn": "^73.0.0", "husky": "^9.1.7", "mocha": "^11.7.6", "prettier": "^3.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a75f3d2..3d6f08b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 1.30.0(zod@4.4.3) '@supabase/supabase-js': specifier: ^2.108.2 - version: 2.111.0 + version: 2.112.2 bplist-parser: specifier: ^0.3.2 version: 0.3.2 @@ -58,7 +58,7 @@ importers: version: 0.2.2 js-yaml: specifier: ^5.2.2 - version: 5.2.2 + version: 5.2.3 node-apk: specifier: ^1.2.1 version: 1.2.1 @@ -109,14 +109,14 @@ importers: specifier: ^10.1.8 version: 10.1.8(eslint@10.8.0) eslint-plugin-unicorn: - specifier: ^72.0.0 - version: 72.0.0(eslint@10.8.0) + specifier: ^73.0.0 + version: 73.0.0(eslint@10.8.0) husky: specifier: ^9.1.7 version: 9.1.7 mocha: specifier: ^11.7.6 - version: 11.7.6 + version: 11.8.0 prettier: specifier: ^3.8.4 version: 3.9.6 @@ -125,13 +125,13 @@ importers: version: 0.4.0 tsx: specifier: ^4.22.4 - version: 4.23.1 + version: 4.23.11 typescript: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: specifier: ^8.61.1 - version: 8.65.0(eslint@10.8.0)(typescript@6.0.3) + version: 8.66.0(eslint@10.8.0)(typescript@6.0.3) packages: @@ -143,158 +143,158 @@ packages: resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -402,32 +402,37 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@supabase/auth-js@2.111.0': - resolution: {integrity: sha512-hbRLgyQZEX0SDyF4LYXpv94qOIQyFATfpT5SIs2V0SisHnisVRkYgiPQWzv80l4S2mO3qof78rmoH9j5zoFsYQ==} + '@supabase/auth-js@2.112.2': + resolution: {integrity: sha512-l1InCp4j98d09LZ6+RgubgF4eVPGBGXcLEhFusLg1qUCHJ2IEkYu5FohKK+eaFmIOwEk0kqG/j/lycw5e15mcQ==} engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.111.0': - resolution: {integrity: sha512-RW/OCsd6MO592zU8ifzP8/f8XzxxIdpb+Up5XaOtE26Fw+3zTp475WX7+GuuktiD1WF8pFUDe6khUPbMp77RCw==} + '@supabase/functions-js@2.112.2': + resolution: {integrity: sha512-oMuSWN0ERmrG9S6kOM0bwhHmESGVl3kMtkZl2dNCU/r89hMiziX4GfD1omNo9QcBDele4N0GwSZ7hdbpuiA35A==} engines: {node: '>=22.0.0'} '@supabase/phoenix@0.4.5': resolution: {integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==} - '@supabase/postgrest-js@2.111.0': - resolution: {integrity: sha512-pcqeDsnWP0lx9GawduYxNZJHeuTm53O7L0SC8RF8tniV3GWIPY6me6OTdnwzdwNUmNy1dzUVtSyIfE6+OflzPQ==} + '@supabase/postgrest-js@2.112.2': + resolution: {integrity: sha512-ewhhtRny/HFRGhUTTg/PsqIatsl8OhW8Eha/Tz4S+SRAXBnuhKei9ZpsQTgL/3XcH9UEwuPQyQgQ9itq7nRQeg==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.111.0': - resolution: {integrity: sha512-6oRf/vZyRwg8f8GbFSJkrD2w4HAu/yTvyMViHXHS+H5hNJzdXCrUR7cP5oW7daT3YlRnzRPY9LcGSJKZAmfMSg==} + '@supabase/realtime-js@2.112.2': + resolution: {integrity: sha512-cd9/CEUJ6Go13FxtfiuC5rYELJtuQzVzTXlGG+XjSppjDS+anq+xo++WQe7ZRUNTuHOCeyKRwmx9Hw/OQJ04ig==} engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.111.0': - resolution: {integrity: sha512-UEViNmTzVOxE8dqUA81wls+n9xgmlvSFfhfwo6QxrO4kQOytCYyw3ciYFoi4XoD4Jl95NJ3jnndHN5iIudWzqw==} + '@supabase/storage-js@2.112.2': + resolution: {integrity: sha512-6jyBq/J1iXOHNpbjCZS7gFcDk49iM1MCJUVkDl71gLd/+XnLDzpUBs8icGebtwiHpl4kVszxIRDYAosbF4Rsig==} engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.111.0': - resolution: {integrity: sha512-9q0/AULthQnWeiDh1vGyjoJZbSY04bu6qHcWit70pqEYn5Kv/dkCPY62Ja1123jEnJbB9Vd2pjY7Kvk/lK3peA==} + '@supabase/supabase-js@2.112.2': + resolution: {integrity: sha512-UyI1epU9B4X51HvNpkmlwTdF20fEcz2vyvrcDKVzFN4jZN41f5iQRqsiIQjAY5OVJD6ljqA/1g9JQeOTvFHpkA==} engines: {node: '>=22.0.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -456,63 +461,63 @@ packages: '@types/yazl@3.3.1': resolution: {integrity: sha512-DIWfCKpsTp6hE5BDBHV3+fIL/bLUF9Bv13iDrWnMlmhQpH67buNvI291ZauQ1xcccxK3FqQ9honnXpq4R8NMuQ==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@xmldom/xmldom@0.9.10': @@ -574,8 +579,8 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.11.4: - resolution: {integrity: sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==} + baseline-browser-mapping@2.11.13: + resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -602,8 +607,8 @@ packages: browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -634,8 +639,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001806: - resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -705,8 +710,9 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-js-compat@3.49.0: - resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js-compat@3.50.0: + resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} + engines: {node: '>=6.4.0'} cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} @@ -761,8 +767,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.396: - resolution: {integrity: sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==} + electron-to-chromium@1.5.403: + resolution: {integrity: sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -793,8 +799,8 @@ packages: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} hasBin: true @@ -815,8 +821,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-unicorn@72.0.0: - resolution: {integrity: sha512-hqO6ksoOHO+ZhdseTuKRVQbx9U7PRO/cv8qAR1mctwzdVO2hYud8uS9luAhp43RJgziYgHAph8eHyipT8GL0ng==} + eslint-plugin-unicorn@73.0.0: + resolution: {integrity: sha512-V0YatLe9nkGhXEXKe2Qljb1EY0sJHwDV0HUF1NKFwtsHh/fU7qGHDgv+6fchzZcgU2/7noHo2gdjnmo0P2uDPw==} engines: {node: '>=22'} peerDependencies: eslint: '>=10.4' @@ -1010,8 +1016,8 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - globals@17.8.0: - resolution: {integrity: sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} gopd@1.2.0: @@ -1156,8 +1162,8 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - js-yaml@5.2.2: - resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + js-yaml@5.2.3: + resolution: {integrity: sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==} hasBin: true jsesc@3.1.0: @@ -1280,8 +1286,8 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mocha@11.7.6: - resolution: {integrity: sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==} + mocha@11.8.0: + resolution: {integrity: sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true @@ -1305,8 +1311,8 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} node-stream-zip@1.16.0: @@ -1516,8 +1522,8 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-javascript@7.0.6: - resolution: {integrity: sha512-ATTK5Q4gFVg0YDp1my2vqygyvhcklD/UV5GIlYHooGTn/NogJqIzpetkD6E5kmuVULqz/S9inUL25XcAgDRJQg==} + serialize-javascript@7.1.0: + resolution: {integrity: sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==} engines: {node: '>=20.0.0'} serve-static@2.2.1: @@ -1660,8 +1666,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.1: - resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + tsx@4.23.11: + resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==} engines: {node: '>=18.0.0'} hasBin: true @@ -1681,8 +1687,8 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typescript-eslint@8.65.0: - resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + typescript-eslint@8.66.0: + resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1700,8 +1706,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1804,82 +1810,82 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@esbuild/aix-ppc64@0.28.1': + '@esbuild/aix-ppc64@0.28.2': optional: true - '@esbuild/android-arm64@0.28.1': + '@esbuild/android-arm64@0.28.2': optional: true - '@esbuild/android-arm@0.28.1': + '@esbuild/android-arm@0.28.2': optional: true - '@esbuild/android-x64@0.28.1': + '@esbuild/android-x64@0.28.2': optional: true - '@esbuild/darwin-arm64@0.28.1': + '@esbuild/darwin-arm64@0.28.2': optional: true - '@esbuild/darwin-x64@0.28.1': + '@esbuild/darwin-x64@0.28.2': optional: true - '@esbuild/freebsd-arm64@0.28.1': + '@esbuild/freebsd-arm64@0.28.2': optional: true - '@esbuild/freebsd-x64@0.28.1': + '@esbuild/freebsd-x64@0.28.2': optional: true - '@esbuild/linux-arm64@0.28.1': + '@esbuild/linux-arm64@0.28.2': optional: true - '@esbuild/linux-arm@0.28.1': + '@esbuild/linux-arm@0.28.2': optional: true - '@esbuild/linux-ia32@0.28.1': + '@esbuild/linux-ia32@0.28.2': optional: true - '@esbuild/linux-loong64@0.28.1': + '@esbuild/linux-loong64@0.28.2': optional: true - '@esbuild/linux-mips64el@0.28.1': + '@esbuild/linux-mips64el@0.28.2': optional: true - '@esbuild/linux-ppc64@0.28.1': + '@esbuild/linux-ppc64@0.28.2': optional: true - '@esbuild/linux-riscv64@0.28.1': + '@esbuild/linux-riscv64@0.28.2': optional: true - '@esbuild/linux-s390x@0.28.1': + '@esbuild/linux-s390x@0.28.2': optional: true - '@esbuild/linux-x64@0.28.1': + '@esbuild/linux-x64@0.28.2': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@esbuild/netbsd-arm64@0.28.2': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@esbuild/netbsd-x64@0.28.2': optional: true - '@esbuild/openbsd-arm64@0.28.1': + '@esbuild/openbsd-arm64@0.28.2': optional: true - '@esbuild/openbsd-x64@0.28.1': + '@esbuild/openbsd-x64@0.28.2': optional: true - '@esbuild/openharmony-arm64@0.28.1': + '@esbuild/openharmony-arm64@0.28.2': optional: true - '@esbuild/sunos-x64@0.28.1': + '@esbuild/sunos-x64@0.28.2': optional: true - '@esbuild/win32-arm64@0.28.1': + '@esbuild/win32-arm64@0.28.2': optional: true - '@esbuild/win32-ia32@0.28.1': + '@esbuild/win32-ia32@0.28.2': optional: true - '@esbuild/win32-x64@0.28.1': + '@esbuild/win32-x64@0.28.2': optional: true '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0)': @@ -1991,37 +1997,37 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@supabase/auth-js@2.111.0': + '@supabase/auth-js@2.112.2': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.111.0': + '@supabase/functions-js@2.112.2': dependencies: tslib: 2.8.1 '@supabase/phoenix@0.4.5': {} - '@supabase/postgrest-js@2.111.0': + '@supabase/postgrest-js@2.112.2': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.111.0': + '@supabase/realtime-js@2.112.2': dependencies: '@supabase/phoenix': 0.4.5 tslib: 2.8.1 - '@supabase/storage-js@2.111.0': + '@supabase/storage-js@2.112.2': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.111.0': + '@supabase/supabase-js@2.112.2': dependencies: - '@supabase/auth-js': 2.111.0 - '@supabase/functions-js': 2.111.0 - '@supabase/postgrest-js': 2.111.0 - '@supabase/realtime-js': 2.111.0 - '@supabase/storage-js': 2.111.0 + '@supabase/auth-js': 2.112.2 + '@supabase/functions-js': 2.112.2 + '@supabase/postgrest-js': 2.112.2 + '@supabase/realtime-js': 2.112.2 + '@supabase/storage-js': 2.112.2 '@types/chai@5.2.3': dependencies: @@ -2048,14 +2054,14 @@ snapshots: dependencies: '@types/node': 26.1.2 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/parser': 8.66.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 eslint: 10.8.0 ignore: 7.0.6 natural-compare: 1.4.0 @@ -2064,41 +2070,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.66.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.65.0(eslint@10.8.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -2106,14 +2112,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.66.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.3 semver: 7.8.5 @@ -2123,20 +2129,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@10.8.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.66.0(eslint@10.8.0)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 '@xmldom/xmldom@0.9.10': {} @@ -2186,7 +2192,7 @@ snapshots: balanced-match@4.0.4: {} - baseline-browser-mapping@2.11.4: {} + baseline-browser-mapping@2.11.13: {} big-integer@1.6.52: {} @@ -2218,13 +2224,13 @@ snapshots: browser-stdout@1.3.1: {} - browserslist@4.28.7: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.11.4 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.396 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.7) + baseline-browser-mapping: 2.11.13 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.403 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) buffer-crc32@1.0.0: {} @@ -2246,7 +2252,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001806: {} + caniuse-lite@1.0.30001809: {} chai@6.2.2: {} @@ -2298,9 +2304,9 @@ snapshots: cookie@0.7.2: {} - core-js-compat@3.49.0: + core-js-compat@3.50.0: dependencies: - browserslist: 4.28.7 + browserslist: 4.28.8 cors@2.8.6: dependencies: @@ -2349,7 +2355,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.396: {} + electron-to-chromium@1.5.403: {} emoji-regex@8.0.0: {} @@ -2371,34 +2377,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.28.1: + esbuild@0.28.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 escalade@3.2.0: {} @@ -2410,19 +2416,19 @@ snapshots: dependencies: eslint: 10.8.0 - eslint-plugin-unicorn@72.0.0(eslint@10.8.0): + eslint-plugin-unicorn@73.0.0(eslint@10.8.0): dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0) '@eslint/css-tree': 4.0.5 - browserslist: 4.28.7 + browserslist: 4.28.8 change-case: 5.4.4 ci-info: 4.4.0 - core-js-compat: 3.49.0 + core-js-compat: 3.50.0 detect-indent: 7.0.2 entities: 4.5.0 eslint: 10.8.0 find-up-simple: 1.0.1 - globals: 17.8.0 + globals: 17.9.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 is-identifier: 1.1.0 @@ -2683,7 +2689,7 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - globals@17.8.0: {} + globals@17.9.0: {} gopd@1.2.0: {} @@ -2784,7 +2790,7 @@ snapshots: js-base64@3.7.8: {} - js-yaml@5.2.2: + js-yaml@5.2.3: dependencies: argparse: 2.0.1 @@ -2893,7 +2899,7 @@ snapshots: dependencies: minipass: 7.1.3 - mocha@11.7.6: + mocha@11.8.0: dependencies: browser-stdout: 1.3.1 chokidar: 4.0.3 @@ -2904,12 +2910,12 @@ snapshots: glob: 10.5.0 he: 1.2.0 is-path-inside: 3.0.3 - js-yaml: 5.2.2 + js-yaml: 5.2.3 log-symbols: 4.1.0 minimatch: 9.0.7 ms: 2.1.3 picocolors: 1.1.1 - serialize-javascript: 7.0.6 + serialize-javascript: 7.1.0 strip-json-comments: 3.1.1 supports-color: 8.1.1 workerpool: 9.3.4 @@ -2931,7 +2937,7 @@ snapshots: node-forge@1.4.0: {} - node-releases@2.0.51: {} + node-releases@2.0.53: {} node-stream-zip@1.16.0: {} @@ -3117,7 +3123,7 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-javascript@7.0.6: {} + serialize-javascript@7.1.0: {} serve-static@2.2.1: dependencies: @@ -3263,9 +3269,9 @@ snapshots: tslib@2.8.1: {} - tsx@4.23.1: + tsx@4.23.11: dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 optionalDependencies: fsevents: 2.3.3 @@ -3291,12 +3297,12 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 - typescript-eslint@8.65.0(eslint@10.8.0)(typescript@6.0.3): + typescript-eslint@8.66.0(eslint@10.8.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.65.0(eslint@10.8.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.65.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0)(typescript@6.0.3))(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.66.0(eslint@10.8.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0)(typescript@6.0.3) eslint: 10.8.0 typescript: 6.0.3 transitivePeerDependencies: @@ -3308,9 +3314,9 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.7): + update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: - browserslist: 4.28.7 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 diff --git a/src/commands/cloud.ts b/src/commands/cloud.ts index 632a2b2..04ef937 100644 --- a/src/commands/cloud.ts +++ b/src/commands/cloud.ts @@ -569,6 +569,9 @@ export const cloudCommand = defineCommand({ excludeFlows, configFile, debug, + // Not warnOut: config problems are worth surfacing even under --json, + // and logger.warn writes to stderr so stdout stays parseable. + warn: (m: string) => logger.warn(m), }); if (debug) { diff --git a/src/mcp/tools/run-cloud-test.ts b/src/mcp/tools/run-cloud-test.ts index 2cb7072..5c94dca 100644 --- a/src/mcp/tools/run-cloud-test.ts +++ b/src/mcp/tools/run-cloud-test.ts @@ -151,6 +151,8 @@ export function registerRunCloudTest(server: McpServer): void { excludeTags: args.excludeTags ?? [], excludeFlows: args.excludeFlows, configFile: args.configFile, + // stdout is the JSON-RPC channel — config warnings must go to stderr. + warn: logStderr, }); const commonRoot = computeCommonRoot( diff --git a/src/services/execution-plan.service.ts b/src/services/execution-plan.service.ts index 777f72b..9c1284c 100644 --- a/src/services/execution-plan.service.ts +++ b/src/services/execution-plan.service.ts @@ -4,45 +4,13 @@ import * as path from 'node:path'; import { getFlowsToRunInSequence, isFlowFile, + isWorkspaceConfigFile, + loadWorkspaceConfig, processDependencies, readDirectory, readTestYamlFileAsJson, - readYamlFileAsJson, } from './execution-plan.utils.js'; - -/** Email notification configuration */ -interface INotificationsConfig { - email?: { - enabled?: boolean; - onSuccess?: boolean; - recipients?: string[]; - }; -} - -/** Workspace configuration from config.yaml */ -interface IWorkspaceConfig { - excludeTags?: null | string[]; - executionOrder?: IExecutionOrder | null; - flows?: null | string[]; - includeTags?: null | string[]; - local?: ILocal | null; - notifications?: INotificationsConfig; - platform?: { - android?: { disableAnimations?: boolean }; - ios?: { disableAnimations?: boolean }; - }; -} - -/** Local execution configuration */ -interface ILocal { - deterministicOrder: boolean | null; -} - -/** Sequential execution configuration */ -interface IExecutionOrder { - continueOnFailure: boolean; - flowsOrder: string[]; -} +import { IWorkspaceConfig } from './workspace-config.schema.js'; /** Options for execution plan generation */ export interface PlanOptions { @@ -52,6 +20,12 @@ export interface PlanOptions { excludeTags?: string[]; includeTags?: string[]; input: string; + /** + * Sink for non-fatal config problems. Injected rather than imported so the + * MCP server can route warnings to stderr — its stdout is the JSON-RPC + * channel. + */ + warn?: (message: string) => void; } /** Execution plan containing all flows to run with metadata and dependencies */ @@ -145,11 +119,13 @@ function filterFlowFiles( * Load workspace configuration from config.yaml/yml if present * @param input - Input directory path * @param unfilteredFlowFiles - List of discovered flow files + * @param warn - Sink for non-fatal config problems * @returns Workspace configuration object (empty if no config file found) */ function getWorkspaceConfig( input: string, unfilteredFlowFiles: string[], + warn: (message: string) => void, ): IWorkspaceConfig { const possibleConfigPaths = new Set( [path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => @@ -161,11 +137,7 @@ function getWorkspaceConfig( possibleConfigPaths.has(path.normalize(file)), ); - const config = configFilePath - ? (readYamlFileAsJson(configFilePath) as IWorkspaceConfig) - : {}; - - return config; + return configFilePath ? loadWorkspaceConfig(configFilePath, warn) : {}; } /** @@ -198,16 +170,20 @@ function extractDeviceCloudOverrides( /** * Generate execution plan for a single flow file * @param normalizedInput - Normalized path to the flow file - * @param configFile - Optional custom config file path + * @param warn - Sink for non-fatal config problems + * @param resolvedConfigFile - Optional absolute path to a custom config file * @returns Execution plan for the single file with dependencies */ async function planSingleFile( normalizedInput: string, - configFile?: string, + warn: (message: string) => void, + resolvedConfigFile?: string, ): Promise { + const inputBasename = path.basename(normalizedInput); if ( - normalizedInput.endsWith('config.yaml') || - normalizedInput.endsWith('config.yml') + inputBasename === 'config.yaml' || + inputBasename === 'config.yml' || + isWorkspaceConfigFile(normalizedInput) ) { throw new Error( 'If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config', @@ -224,13 +200,22 @@ async function planSingleFile( } let workspaceConfig: IWorkspaceConfig | undefined; - if (configFile) { - const configFilePath = path.resolve(process.cwd(), configFile); - if (!fs.existsSync(configFilePath)) { - throw new Error(`Config file does not exist: ${configFilePath}`); + if (resolvedConfigFile) { + if (!fs.existsSync(resolvedConfigFile)) { + throw new Error(`Config file does not exist: ${resolvedConfigFile}`); } - workspaceConfig = readYamlFileAsJson(configFilePath) as IWorkspaceConfig; + workspaceConfig = loadWorkspaceConfig(resolvedConfigFile, warn); + + // Sequencing is resolved against a workspace's discovered flows, which a + // single-file input doesn't have — so executionOrder is ignored here. Say so + // rather than accepting a config that reads as if it applied (dcd-cli#110). + if (workspaceConfig.executionOrder?.flowsOrder.length) { + warn( + `Warning: \`executionOrder\` in ${resolvedConfigFile} is ignored when a single flow file is passed.\n` + + `Pass the workspace folder instead so the named flows can be discovered and sequenced.`, + ); + } } const checkedDependancies = await checkDependencies(normalizedInput); @@ -249,7 +234,7 @@ async function planSingleFile( * @param workspaceConfig - Workspace configuration containing flow globs * @param normalizedInput - Normalized path to the workspace directory * @param unfilteredFlowFiles - List of all discovered flow files - * @param configFile - Optional custom config file path + * @param resolvedConfigFile - Optional absolute path to a custom config file * @param excludeFlows - --exclude-flows patterns to re-apply to glob matches * @returns Filtered list of flow file paths matching the globs */ @@ -257,9 +242,24 @@ async function applyFlowGlobs( workspaceConfig: IWorkspaceConfig, normalizedInput: string, unfilteredFlowFiles: string[], - configFile?: string, + resolvedConfigFile?: string, excludeFlows?: string[], ): Promise { + // Both branches compare absolute paths, so `--config ./x.yml` and + // `--config /abs/x.yml` behave identically, and a same-named file in a + // sibling directory is never mistaken for the active config. + const activeConfig = resolvedConfigFile + ? path.normalize(resolvedConfigFile) + : undefined; + const isExcludedConfig = (absolutePath: string): boolean => { + const base = path.basename(absolutePath); + if (base === 'config.yaml' || base === 'config.yml') return true; + return ( + activeConfig !== undefined && + path.normalize(absolutePath) === activeConfig + ); + }; + if (workspaceConfig.flows) { const globs = workspaceConfig.flows.map((g) => g); // fs.globSync lands in Node 22; the CLI's `engines.node` already requires it. @@ -273,31 +273,19 @@ async function applyFlowGlobs( } }); + // Resolve before filtering: glob matches are relative to normalizedInput, + // so comparing them against the config path only ever worked when the + // config happened to sit at the workspace root. const globbedFlowFiles = matchedFiles - .filter((file: string) => { - if (file === 'config.yaml' || file === 'config.yml') return false; - if (configFile && file === path.basename(configFile)) return false; - if (!file.endsWith('.yaml') && !file.endsWith('.yml')) return false; - const pathParts = file.split(path.sep); - for (const part of pathParts) { - if (part.endsWith('.app')) return false; - } - - return true; - }) - .map((file) => path.resolve(normalizedInput, file)); + .map((file) => path.resolve(normalizedInput, file)) + .filter((file) => !isExcludedConfig(file) && isFlowFile(file)); // Re-globbing from disk bypasses the earlier --exclude-flows filter, so // re-apply it here or excluded flows sneak back in via `flows:` globs. return filterFlowFiles(globbedFlowFiles, excludeFlows); } - return unfilteredFlowFiles.filter( - (file) => - !file.endsWith('config.yaml') && - !file.endsWith('config.yml') && - (!configFile || !file.endsWith(configFile)), - ); + return unfilteredFlowFiles.filter((file) => !isExcludedConfig(file)); } /** @@ -379,9 +367,13 @@ export async function plan(options: PlanOptions): Promise { excludeFlows, configFile, debug = false, + warn = (message: string) => console.warn(message), } = options; const normalizedInput = path.normalize(input); const flowMetadata: Record> = {}; + const resolvedConfigFile = configFile + ? path.resolve(process.cwd(), configFile) + : undefined; if (!fs.existsSync(normalizedInput)) { throw new Error( @@ -390,7 +382,7 @@ export async function plan(options: PlanOptions): Promise { } if (fs.lstatSync(normalizedInput).isFile()) { - return planSingleFile(normalizedInput, configFile); + return planSingleFile(normalizedInput, warn, resolvedConfigFile); } let unfilteredFlowFiles = await readDirectory(normalizedInput, isFlowFile); @@ -405,25 +397,50 @@ export async function plan(options: PlanOptions): Promise { unfilteredFlowFiles = filterFlowFiles(unfilteredFlowFiles, excludeFlows); let workspaceConfig: IWorkspaceConfig; - if (configFile) { - const configFilePath = path.resolve(process.cwd(), configFile); - if (!fs.existsSync(configFilePath)) { - throw new Error(`Config file does not exist: ${configFilePath}`); + if (resolvedConfigFile) { + if (!fs.existsSync(resolvedConfigFile)) { + throw new Error(`Config file does not exist: ${resolvedConfigFile}`); } - workspaceConfig = readYamlFileAsJson(configFilePath) as IWorkspaceConfig; + workspaceConfig = loadWorkspaceConfig(resolvedConfigFile, warn); } else { - workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles); + workspaceConfig = getWorkspaceConfig( + normalizedInput, + unfilteredFlowFiles, + warn, + ); } unfilteredFlowFiles = await applyFlowGlobs( workspaceConfig, normalizedInput, unfilteredFlowFiles, - configFile, + resolvedConfigFile, excludeFlows, ); + // The exclusions above are filename-based: they only catch + // `config.yaml`/`config.yml` and the active --config target. Any other + // workspace config sharing the folder (a second CI workflow's, say) is still + // on the list and would be parsed as a flow, so drop config-shaped files by + // shape — see dcd-cli#99. + const configShapedFiles = new Set( + unfilteredFlowFiles.filter((file) => isWorkspaceConfigFile(file)), + ); + if (configShapedFiles.size > 0) { + if (debug) { + console.log( + `[DEBUG] Skipping ${configShapedFiles.size} workspace config file(s): ${[ + ...configShapedFiles, + ].join(', ')}`, + ); + } + + unfilteredFlowFiles = unfilteredFlowFiles.filter( + (file) => !configShapedFiles.has(file), + ); + } + if (unfilteredFlowFiles.length === 0) { const error = workspaceConfig.flows ? new Error( diff --git a/src/services/execution-plan.utils.ts b/src/services/execution-plan.utils.ts index b117759..dbb23ee 100644 --- a/src/services/execution-plan.utils.ts +++ b/src/services/execution-plan.utils.ts @@ -3,6 +3,12 @@ import * as yaml from 'js-yaml'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { + IWorkspaceConfig, + parseWorkspaceConfig, + WORKSPACE_CONFIG_KEYS, +} from './workspace-config.schema.js'; + const commandsThatRequireFiles = new Set(['addMedia', 'runFlow', 'runScript']); export function getFlowsToRunInSequence( @@ -60,25 +66,43 @@ export function isFlowFile(filePath: string): boolean { return filePath.endsWith('.yaml') || filePath.endsWith('.yml'); } -export const readYamlFileAsJson = (filePath: string) => { +/** + * True when a YAML file is a workspace config rather than a runnable flow. + * + * A flow is either `front matter --- steps` or a bare steps array; a + * single-document top-level map carrying workspace-config keys is neither, and + * left in the flow list it blows up processDependencies with "Expected an array + * of steps". Detection is by shape, not filename, so several named configs can + * coexist in one folder (dcd-cli#99). Requiring a recognised config key — not + * just "single document, top-level map" — keeps a flow that is merely *missing* + * its `---` separator loud rather than silently dropped. + * + * @param filePath - Path to the YAML file to classify + * @returns Whether the file is a workspace config rather than a flow + */ +export function isWorkspaceConfigFile(filePath: string): boolean { + let parsed; try { - const normalizedPath = path.normalize(filePath); - const yamlText = fs.readFileSync(normalizedPath, 'utf8'); + parsed = readTestYamlFileAsJson(filePath); + } catch { + // Unparseable — leave it in the flow list so the existing error path reports it. + return false; + } - const result = yaml.load(yamlText); + const { config, testSteps } = parsed; + if (config !== null) return false; // has `---` front matter → flow + if (Array.isArray(testSteps)) return false; // bare steps array → flow + if (!testSteps || typeof testSteps !== 'object') return false; - // Ensure includeTags and excludeTags are always arrays if present - if (result && typeof result === 'object') { - if ('includeTags' in result && !Array.isArray(result.includeTags)) { - result.includeTags = result.includeTags ? [result.includeTags] : []; - } + return Object.keys(testSteps).some((key) => WORKSPACE_CONFIG_KEYS.has(key)); +} - if ('excludeTags' in result && !Array.isArray(result.excludeTags)) { - result.excludeTags = result.excludeTags ? [result.excludeTags] : []; - } - } +export const readYamlFileAsJson = (filePath: string) => { + try { + const normalizedPath = path.normalize(filePath); + const yamlText = fs.readFileSync(normalizedPath, 'utf8'); - return result; + return yaml.load(yamlText); } catch (error) { throw new Error(`Error parsing YAML file ${filePath}: ${error}`, { cause: error, @@ -86,6 +110,26 @@ export const readYamlFileAsJson = (filePath: string) => { } }; +/** + * Load and validate a workspace config file. + * + * The single chokepoint for reading a config: every caller gets a + * runtime-validated object instead of an unchecked `as IWorkspaceConfig` cast. + * Scalar-to-array coercion for `includeTags`/`excludeTags` lives in the schema, + * so `readYamlFileAsJson` stays a plain YAML read. + * + * @param filePath - Path to the config file + * @param warn - Sink for non-fatal problems (unrecognised keys) + * @returns The validated workspace config + * @throws Error if the file is unparseable or the config is invalid + */ +export function loadWorkspaceConfig( + filePath: string, + warn: (message: string) => void, +): IWorkspaceConfig { + return parseWorkspaceConfig(readYamlFileAsJson(filePath), { filePath, warn }); +} + export const readTestYamlFileAsJson = (filePath: string) => { try { const normalizedPath = path.normalize(filePath); diff --git a/src/services/workspace-config.schema.ts b/src/services/workspace-config.schema.ts new file mode 100644 index 0000000..960cee9 --- /dev/null +++ b/src/services/workspace-config.schema.ts @@ -0,0 +1,255 @@ +import * as yaml from 'js-yaml'; +import { z } from 'zod'; + +/** + * Runtime schema for a workspace `config.yaml`. + * + * This is the single source of truth for the config's shape — the TypeScript + * type is inferred from it (`IWorkspaceConfig`) rather than declared alongside + * it, so the compile-time and runtime views cannot drift. Before this existed + * the config was `yaml.load`ed and straight-cast, which meant an + * `executionOrder` written in the wrong shape was silently ignored and every + * flow ran in parallel (dcd-cli#110). + */ + +/** + * Tags may be written as a bare scalar (`includeTags: smoke`) or a list. + * Scalars are wrapped, and primitive members are coerced to strings so a + * YAML-numeric tag (`includeTags: [2]`) still compares against flow tags + * instead of silently matching nothing. + */ +const tagList = z.preprocess((value) => { + if (value === null || value === undefined) return value; + const members = Array.isArray(value) ? value : [value]; + return members.map((member) => + typeof member === 'number' || typeof member === 'boolean' + ? String(member) + : member, + ); +}, z.array(z.string())); + +/** Sequential execution configuration */ +const ExecutionOrderSchema = z.object({ + // Defaults to true: a failing flow does not stop the rest of the sequence. + // Declared here so the default lives in one place instead of being + // re-specified at each read site. + continueOnFailure: z.boolean().default(true), + flowsOrder: z.array(z.string()), +}); + +/** + * `looseObject`, not `object`: unknown keys are reported as warnings but must + * survive parsing, because the whole config is forwarded to the API as + * `fields.workspaceConfig` and stripping keys would silently alter that + * payload. + */ +export const WorkspaceConfigSchema = z.looseObject({ + excludeTags: tagList.nullish(), + executionOrder: ExecutionOrderSchema.nullish(), + flows: z.array(z.string()).nullish(), + includeTags: tagList.nullish(), + local: z + .looseObject({ deterministicOrder: z.boolean().nullish() }) + .nullish(), + notifications: z + .looseObject({ + email: z + .looseObject({ + enabled: z.boolean().optional(), + onSuccess: z.boolean().optional(), + recipients: z.array(z.string()).optional(), + }) + .optional(), + }) + .nullish(), + platform: z + .looseObject({ + android: z + .looseObject({ disableAnimations: z.boolean().optional() }) + .optional(), + ios: z + .looseObject({ disableAnimations: z.boolean().optional() }) + .optional(), + }) + .nullish(), +}); + +/** Workspace configuration from config.yaml */ +export type IWorkspaceConfig = z.infer; + +/** + * Top-level keys that only ever appear in a workspace config, derived from the + * schema so the two can't drift. Deliberately excludes keys Maestro also allows + * in flow front matter — appId, name, tags, env, onFlowStart, onFlowComplete, + * jsEngine — because this set also drives config-vs-flow detection + * (`isWorkspaceConfigFile`). + */ +export const WORKSPACE_CONFIG_KEYS: ReadonlySet = new Set( + Object.keys(WorkspaceConfigSchema.shape), +); + +/** + * Near-misses that aren't just a casing slip on a real key. Keyed lowercase. + */ +const KEY_ALIASES: Record = { + continueonfailure: 'executionOrder.continueOnFailure', + excludetag: 'excludeTags', + floworder: 'executionOrder.flowsOrder', + flowsorder: 'executionOrder.flowsOrder', + includetag: 'includeTags', + tags: 'includeTags / excludeTags', +}; + +/** + * Suggest the key the author probably meant. + * @param unknownKey - Unrecognised top-level key from the config + * @returns The suggested key name, or undefined if there's no close match + */ +function suggestKey(unknownKey: string): string | undefined { + const lowered = unknownKey.toLowerCase(); + const casingSlip = [...WORKSPACE_CONFIG_KEYS].find( + (key) => key.toLowerCase() === lowered, + ); + + return casingSlip ?? KEY_ALIASES[lowered]; +} + +/** + * Describe how an `executionOrder` value is malformed, in prose. + * + * Split out so the most likely mistake — a bare list of flow names — gets a + * message showing the expected shape rather than a generic schema dump. + * + * @param value - The raw `executionOrder` value from the config + * @returns A description of the problem, or undefined if the shape is valid + */ +function describeExecutionOrderShape(value: unknown): string | undefined { + if (value === null || value === undefined) return undefined; + if (Array.isArray(value)) return 'a bare list of flow names'; + if (typeof value !== 'object') return `a ${typeof value} value`; + + const { flowsOrder } = value as Record; + if (flowsOrder === undefined) return 'a map with no `flowsOrder` key'; + if (!Array.isArray(flowsOrder)) { + return 'a map whose `flowsOrder` is not a list'; + } + + return undefined; +} + +/** Indent a YAML fragment so it reads as a block inside an error message. */ +function indentYaml(value: unknown): string { + return yaml + .dump(value, { lineWidth: -1 }) + .trimEnd() + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); +} + +/** + * Build the error for a malformed `executionOrder`, showing what was found + * next to what was expected. + * @param filePath - Path to the config file, for the message + * @param problem - Prose description from describeExecutionOrderShape + * @param found - The raw `executionOrder` value that failed + * @returns The error to throw + */ +function executionOrderError( + filePath: string, + problem: string, + found: unknown, +): Error { + return new Error( + `Invalid \`executionOrder\` in ${filePath}\n\n` + + `\`executionOrder\` must be a map containing a \`flowsOrder\` list, but it is ${problem}.\n\n` + + `Found:\n${indentYaml({ executionOrder: found })}\n\n` + + `Expected:\n${indentYaml({ + executionOrder: { + continueOnFailure: true, + flowsOrder: ['first-flow', 'second-flow'], + }, + })}\n\n` + + `Without \`flowsOrder\` the flows are not sequenced — they all run in parallel.`, + ); +} + +/** + * Validate a parsed workspace config. + * + * Hard errors (thrown): the config isn't a map, or `executionOrder` is present + * but malformed. Both are unambiguous mistakes with no valid interpretation, so + * failing fast beats a warning that scrolls past in CI. + * + * Soft problems (warned): unknown top-level keys. These are preserved, not + * stripped, and warned about once. + * + * @param raw - The result of loading the YAML file + * @param options - Config file path (for messages) and a warning sink + * @returns The validated config, with defaults applied + * @throws Error if the config is not a map or `executionOrder` is malformed + */ +export function parseWorkspaceConfig( + raw: unknown, + options: { filePath: string; warn: (message: string) => void }, +): IWorkspaceConfig { + const { filePath, warn } = options; + + if (raw === null || raw === undefined) return {}; + + if (typeof raw !== 'object' || Array.isArray(raw)) { + throw new Error( + `Invalid workspace config in ${filePath}\n\n` + + `Expected a map of configuration keys (${[...WORKSPACE_CONFIG_KEYS].join( + ', ', + )}), but found ${Array.isArray(raw) ? 'a list' : `a ${typeof raw} value`}.`, + ); + } + + const rawConfig = raw as Record; + + // Checked ahead of the schema so this specific mistake gets a targeted + // message; the schema would otherwise report "expected object, received array". + const executionOrderProblem = describeExecutionOrderShape( + rawConfig.executionOrder, + ); + if (executionOrderProblem) { + throw executionOrderError( + filePath, + executionOrderProblem, + rawConfig.executionOrder, + ); + } + + const unknownKeys = Object.keys(rawConfig).filter( + (key) => !WORKSPACE_CONFIG_KEYS.has(key), + ); + if (unknownKeys.length > 0) { + const lines = unknownKeys.map((key) => { + const suggestion = suggestKey(key); + return suggestion + ? ` ${key} — did you mean ${suggestion}?` + : ` ${key}`; + }); + + warn( + `Warning: unrecognised key(s) in ${filePath} — these are ignored:\n` + + `${lines.join('\n')}\n\n` + + `Supported keys: ${[...WORKSPACE_CONFIG_KEYS].join(', ')}`, + ); + } + + const result = WorkspaceConfigSchema.safeParse(rawConfig); + if (!result.success) { + const issues = result.error.issues + .map((issue) => { + const location = issue.path.length > 0 ? issue.path.join('.') : '(root)'; + return ` ${location}: ${issue.message}`; + }) + .join('\n'); + + throw new Error(`Invalid workspace config in ${filePath}\n\n${issues}`); + } + + return result.data; +} diff --git a/test/fixtures/basic-config.yaml b/test/fixtures/basic-config.yaml index 0671d08..3c49ac4 100644 --- a/test/fixtures/basic-config.yaml +++ b/test/fixtures/basic-config.yaml @@ -1,8 +1,7 @@ flows: - ./**/*.yaml - ./*.yaml -tags: - include: - - smoke - exclude: - - slow \ No newline at end of file +includeTags: + - smoke +excludeTags: + - slow diff --git a/test/fixtures/tag-filtering-config.yaml b/test/fixtures/tag-filtering-config.yaml index c08d93c..bb28996 100644 --- a/test/fixtures/tag-filtering-config.yaml +++ b/test/fixtures/tag-filtering-config.yaml @@ -1,8 +1,7 @@ flows: - ./**/*.yaml -tags: - include: - - smoke - exclude: - - slow - - integration \ No newline at end of file +includeTags: + - smoke +excludeTags: + - slow + - integration diff --git a/test/integration/cloud.integration.test.ts b/test/integration/cloud.integration.test.ts index 7d052af..39d77d5 100644 --- a/test/integration/cloud.integration.test.ts +++ b/test/integration/cloud.integration.test.ts @@ -287,6 +287,211 @@ appId: com.example.app }); }); + // Regression cover for dcd-cli#99: config files were excluded from flow + // discovery by *filename* (literal config.yaml/config.yml plus the exact + // --config target), so a second workspace config sharing the folder was + // parsed as a flow and blew up with "Expected an array of steps". + // These must pass the flow *directory* — the --config tests below pass a + // single file, which short-circuits into planSingleFile and never reaches + // flow discovery. + describe('workspace config discovery', () => { + let workspaceDir: string; + let buildConfig: string; + let updateConfig: string; + let globConfig: string; + + before(() => { + workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dcd-test-ws-')); + + fs.writeFileSync( + path.join(workspaceDir, 'flow.yaml'), + `appId: com.example.app +--- +- launchApp +- tapOn: "Login" +`, + ); + + // A genuine flow whose name merely ends in "config.yaml". The old + // unanchored endsWith() dropped it; it must run. + fs.writeFileSync( + path.join(workspaceDir, 'smoke-config.yaml'), + `appId: com.example.app +name: smoke-config +--- +- launchApp +`, + ); + + // Two sibling workspace configs with custom names, as a folder serving + // several CI workflows would have. Neither is a flow. + buildConfig = path.join(workspaceDir, 'config_build.yml'); + fs.writeFileSync( + buildConfig, + `flows: + - ./**/*.yaml +includeTags: + - build +`, + ); + + // Deliberately carries no tag or flow filtering, so when it is the active + // --config it can't mask the bug by filtering its sibling away first. + updateConfig = path.join(workspaceDir, 'config_update.yml'); + fs.writeFileSync( + updateConfig, + `platform: + android: + disableAnimations: true +`, + ); + + globConfig = path.join(workspaceDir, 'config_glob.yml'); + fs.writeFileSync( + globConfig, + `flows: + - ./**/*.yaml + - ./**/*.yml +`, + ); + }); + + after(() => { + if (fs.existsSync(workspaceDir)) { + fs.rmSync(workspaceDir, { force: true, recursive: true }); + } + }); + + it('should ignore sibling config files not named in --config', async () => { + const command = `${CLI} cloud ${androidAppFile} "${workspaceDir}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --config "${updateConfig}" --dry-run`; + + const { stdout } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('The following tests would have been run'); + expect(stdout).to.include('flow.yaml'); + expect(stdout).to.not.include('config_build.yml'); + expect(stdout).to.not.include('config_glob.yml'); + expect(stdout).to.not.include('Expected an array of steps'); + }); + + it('should ignore config-shaped files when no --config is passed', async () => { + const command = `${CLI} cloud ${androidAppFile} "${workspaceDir}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --debug --dry-run`; + + const { stdout } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('flow.yaml'); + expect(stdout).to.include('[DEBUG] Skipping 3 workspace config file(s)'); + expect(stdout).to.not.include('Expected an array of steps'); + }); + + it('should ignore config-shaped files matched by a flows glob', async () => { + const command = `${CLI} cloud ${androidAppFile} "${workspaceDir}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --config "${globConfig}" --dry-run`; + + const { stdout } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('flow.yaml'); + expect(stdout).to.not.include('config_build.yml'); + expect(stdout).to.not.include('config_update.yml'); + expect(stdout).to.not.include('Expected an array of steps'); + }); + + it('should run a flow whose filename merely ends in config.yaml', async () => { + const command = `${CLI} cloud ${androidAppFile} "${workspaceDir}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --config "${updateConfig}" --dry-run`; + + const { stdout } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('smoke-config.yaml'); + }); + + it('should reject a custom-named config passed as the flow input', async () => { + const command = `${CLI} cloud ${androidAppFile} "${buildConfig}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --dry-run`; + + const { output } = await runExpectingFailure(command); + expect(output).to.include('pass the workspace folder path'); + expect(output).to.not.include('Expected an array of steps'); + }); + }); + + // dcd-cli#110: a `config.yaml` whose executionOrder was the wrong *shape* was + // silently ignored and every flow ran in parallel — same cost, wrong + // semantics, green run. These assert the shape is now validated. + describe('executionOrder validation', () => { + let workspaceDir: string; + + /** Point the run at a workspace whose config.yaml holds `configBody`. */ + const commandWithConfig = (configBody: string): string => { + fs.writeFileSync(path.join(workspaceDir, 'config.yaml'), configBody); + return `${CLI} cloud ${androidAppFile} "${workspaceDir}" --api-key ${mockApiKey} --api-url ${mockApiUrl} --dry-run`; + }; + + before(() => { + workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dcd-test-order-')); + + for (const name of ['a', 'b', 'c']) { + fs.writeFileSync( + path.join(workspaceDir, `${name}.yaml`), + `appId: com.example.app +--- +- launchApp +`, + ); + } + }); + + after(() => { + if (fs.existsSync(workspaceDir)) { + fs.rmSync(workspaceDir, { force: true, recursive: true }); + } + }); + + it('should reject a bare-list executionOrder instead of running in parallel', async () => { + const command = commandWithConfig(`executionOrder: + - a.yaml + - b.yaml + - c.yaml +`); + + const { output } = await runExpectingFailure(command); + expect(output).to.include('Invalid `executionOrder`'); + expect(output).to.include('flowsOrder'); + // The whole point: it must not quietly proceed to a parallel run. + expect(output).to.not.include('The following tests would have been run'); + }); + + it('should reject an executionOrder map with no flowsOrder', async () => { + const command = commandWithConfig(`executionOrder: + continueOnFailure: true +`); + + const { output } = await runExpectingFailure(command); + expect(output).to.include('Invalid `executionOrder`'); + expect(output).to.include('no `flowsOrder` key'); + }); + + it('should sequence flows for a well-formed executionOrder', async () => { + const command = commandWithConfig(`executionOrder: + continueOnFailure: true + flowsOrder: + - a.yaml + - b.yaml +`); + + const { stdout } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('Sequential flows'); + expect(stdout).to.include('a.yaml'); + expect(stdout).to.include('b.yaml'); + }); + + it('should warn about unrecognised config keys without failing the run', async () => { + const command = commandWithConfig(`flowOrder: + - a.yaml +flowTimeout: 120000 +`); + + const { stdout, stderr } = await exec(command, { timeout: 15_000 }); + expect(stdout).to.include('The following tests would have been run'); + expect(stderr).to.include('flowOrder'); + expect(stderr).to.include('executionOrder.flowsOrder'); + expect(stderr).to.include('flowTimeout'); + }); + }); + describe('file and binary management', () => { it('should support app binary ID instead of file', async () => { const command = `${CLI} cloud --app-binary-id test-binary-123 ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --dry-run`;