From e85afab3e6af1f4969346c0de76740648bea1751 Mon Sep 17 00:00:00 2001 From: Lucas Tettamanti <757326+lkzwieder@users.noreply.github.com> Date: Mon, 26 Jan 2026 00:13:03 -0300 Subject: [PATCH] vitest --- package-lock.json | 1711 ++++++++++++++++- package.json | 7 +- src/modules/3-turn-engine/fsm.test.js | 565 ++++++ src/modules/3-turn-engine/orderModel.test.js | 748 +++++++ .../3-turn-engine/stateHandlers/utils.test.js | 448 +++++ vitest.config.js | 13 + 6 files changed, 3490 insertions(+), 2 deletions(-) create mode 100644 src/modules/3-turn-engine/fsm.test.js create mode 100644 src/modules/3-turn-engine/orderModel.test.js create mode 100644 src/modules/3-turn-engine/stateHandlers/utils.test.js create mode 100644 vitest.config.js diff --git a/package-lock.json b/package-lock.json index 7c81145..6a35e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,70 @@ "zod": "^4.3.4" }, "devDependencies": { + "@vitest/coverage-v8": "^4.0.18", "dbmate": "^2.0.0", - "nodemon": "^3.0.3" + "nodemon": "^3.0.3", + "vitest": "^4.0.18" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@dbmate/darwin-arm64": { @@ -122,6 +184,1000 @@ "win32" ] }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -171,6 +1227,28 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -277,6 +1355,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -470,6 +1558,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -482,12 +1577,64 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -497,6 +1644,16 @@ "node": ">= 0.6" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -734,6 +1891,13 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -834,12 +1998,119 @@ "node": ">=0.12.0" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -928,6 +2199,25 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1022,6 +2312,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1070,6 +2371,13 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -1159,6 +2467,13 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1172,6 +2487,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -1292,6 +2636,51 @@ "node": ">=0.10.0" } }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1454,6 +2843,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1467,6 +2863,16 @@ "node": ">=10" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1476,6 +2882,13 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1485,6 +2898,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1498,6 +2918,81 @@ "node": ">=4" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1586,6 +3081,220 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index d365668..2238de9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "scripts": { "start": "node index.js", "dev": "nodemon index.js", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", "migrate:up": "dbmate up", "migrate:down": "dbmate down", "migrate:redo": "dbmate rollback && dbmate up", @@ -27,7 +30,9 @@ "zod": "^4.3.4" }, "devDependencies": { + "@vitest/coverage-v8": "^4.0.18", "dbmate": "^2.0.0", - "nodemon": "^3.0.3" + "nodemon": "^3.0.3", + "vitest": "^4.0.18" } } diff --git a/src/modules/3-turn-engine/fsm.test.js b/src/modules/3-turn-engine/fsm.test.js new file mode 100644 index 0000000..d2bea34 --- /dev/null +++ b/src/modules/3-turn-engine/fsm.test.js @@ -0,0 +1,565 @@ +/** + * Tests para fsm.js + */ +import { describe, it, expect } from 'vitest'; +import { + ConversationState, + ALL_STATES, + INTENTS_BY_STATE, + shouldReturnToCart, + hasCartItems, + hasPendingItems, + hasReadyPendingItems, + hasShippingInfo, + hasPaymentInfo, + isPaid, + deriveNextState, + validateTransition, + safeNextState, +} from './fsm.js'; + +// ───────────────────────────────────────────────────────────── +// Constants +// ───────────────────────────────────────────────────────────── + +describe('ConversationState', () => { + it('tiene todos los estados definidos', () => { + expect(ConversationState.IDLE).toBe('IDLE'); + expect(ConversationState.CART).toBe('CART'); + expect(ConversationState.SHIPPING).toBe('SHIPPING'); + expect(ConversationState.PAYMENT).toBe('PAYMENT'); + expect(ConversationState.WAITING_WEBHOOKS).toBe('WAITING_WEBHOOKS'); + expect(ConversationState.AWAITING_HUMAN).toBe('AWAITING_HUMAN'); + }); + + it('ALL_STATES contiene todos', () => { + expect(ALL_STATES).toContain('IDLE'); + expect(ALL_STATES).toContain('CART'); + expect(ALL_STATES).toContain('SHIPPING'); + expect(ALL_STATES).toContain('PAYMENT'); + expect(ALL_STATES).toContain('WAITING_WEBHOOKS'); + expect(ALL_STATES).toContain('AWAITING_HUMAN'); + expect(ALL_STATES).toHaveLength(6); + }); + + it('INTENTS_BY_STATE define intents para cada estado', () => { + expect(INTENTS_BY_STATE[ConversationState.IDLE]).toContain('greeting'); + expect(INTENTS_BY_STATE[ConversationState.CART]).toContain('add_to_cart'); + expect(INTENTS_BY_STATE[ConversationState.SHIPPING]).toContain('provide_address'); + expect(INTENTS_BY_STATE[ConversationState.PAYMENT]).toContain('select_payment'); + }); +}); + +// ───────────────────────────────────────────────────────────── +// hasCartItems +// ───────────────────────────────────────────────────────────── + +describe('hasCartItems', () => { + it('retorna true si cart tiene items', () => { + const order = { cart: [{ woo_id: 1, qty: 1 }] }; + expect(hasCartItems(order)).toBe(true); + }); + + it('retorna false si cart está vacío', () => { + const order = { cart: [] }; + expect(hasCartItems(order)).toBe(false); + }); + + it('retorna false si cart es undefined', () => { + const order = {}; + expect(hasCartItems(order)).toBe(false); + }); + + it('retorna false si order es null', () => { + expect(hasCartItems(null)).toBe(false); + }); + + it('retorna false si order es undefined', () => { + expect(hasCartItems(undefined)).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// hasPendingItems +// ───────────────────────────────────────────────────────────── + +describe('hasPendingItems', () => { + it('retorna true si hay NEEDS_TYPE', () => { + const order = { pending: [{ status: 'NEEDS_TYPE' }] }; + expect(hasPendingItems(order)).toBe(true); + }); + + it('retorna true si hay NEEDS_QUANTITY', () => { + const order = { pending: [{ status: 'NEEDS_QUANTITY' }] }; + expect(hasPendingItems(order)).toBe(true); + }); + + it('retorna false si solo hay READY', () => { + const order = { pending: [{ status: 'READY' }] }; + expect(hasPendingItems(order)).toBe(false); + }); + + it('retorna false si pending está vacío', () => { + const order = { pending: [] }; + expect(hasPendingItems(order)).toBe(false); + }); + + it('retorna false si order es null', () => { + expect(hasPendingItems(null)).toBe(false); + }); + + it('detecta entre múltiples items', () => { + const order = { + pending: [ + { status: 'READY' }, + { status: 'NEEDS_TYPE' }, + ] + }; + expect(hasPendingItems(order)).toBe(true); + }); +}); + +// ───────────────────────────────────────────────────────────── +// hasReadyPendingItems +// ───────────────────────────────────────────────────────────── + +describe('hasReadyPendingItems', () => { + it('retorna true si hay READY', () => { + const order = { pending: [{ status: 'READY' }] }; + expect(hasReadyPendingItems(order)).toBe(true); + }); + + it('retorna false si no hay READY', () => { + const order = { pending: [{ status: 'NEEDS_TYPE' }] }; + expect(hasReadyPendingItems(order)).toBe(false); + }); + + it('retorna false si pending vacío', () => { + const order = { pending: [] }; + expect(hasReadyPendingItems(order)).toBe(false); + }); + + it('retorna false si order es null', () => { + expect(hasReadyPendingItems(null)).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// hasShippingInfo +// ───────────────────────────────────────────────────────────── + +describe('hasShippingInfo', () => { + it('retorna true para pickup (no necesita dirección)', () => { + const order = { is_delivery: false }; + expect(hasShippingInfo(order)).toBe(true); + }); + + it('retorna true para delivery con dirección', () => { + const order = { is_delivery: true, shipping_address: 'Calle Falsa 123' }; + expect(hasShippingInfo(order)).toBe(true); + }); + + it('retorna false para delivery sin dirección', () => { + const order = { is_delivery: true, shipping_address: null }; + expect(hasShippingInfo(order)).toBe(false); + }); + + it('retorna false si is_delivery es null', () => { + const order = { is_delivery: null }; + expect(hasShippingInfo(order)).toBe(false); + }); + + it('retorna false para order vacío', () => { + expect(hasShippingInfo({})).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// hasPaymentInfo +// ───────────────────────────────────────────────────────────── + +describe('hasPaymentInfo', () => { + it('retorna true para cash', () => { + const order = { payment_type: 'cash' }; + expect(hasPaymentInfo(order)).toBe(true); + }); + + it('retorna true para link', () => { + const order = { payment_type: 'link' }; + expect(hasPaymentInfo(order)).toBe(true); + }); + + it('retorna false para null', () => { + const order = { payment_type: null }; + expect(hasPaymentInfo(order)).toBe(false); + }); + + it('retorna false para undefined', () => { + const order = {}; + expect(hasPaymentInfo(order)).toBe(false); + }); + + it('retorna false para otros valores', () => { + const order = { payment_type: 'bitcoin' }; + expect(hasPaymentInfo(order)).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// isPaid +// ───────────────────────────────────────────────────────────── + +describe('isPaid', () => { + it('retorna true si is_paid es true', () => { + const order = { is_paid: true }; + expect(isPaid(order)).toBe(true); + }); + + it('retorna false si is_paid es false', () => { + const order = { is_paid: false }; + expect(isPaid(order)).toBe(false); + }); + + it('retorna false si is_paid es undefined', () => { + const order = {}; + expect(isPaid(order)).toBe(false); + }); + + it('retorna false si order es null', () => { + expect(isPaid(null)).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// shouldReturnToCart +// ───────────────────────────────────────────────────────────── + +describe('shouldReturnToCart', () => { + describe('no redirige si ya está en CART o IDLE', () => { + it('retorna false en CART', () => { + const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } }; + expect(shouldReturnToCart(ConversationState.CART, nlu)).toBe(false); + }); + + it('retorna false en IDLE', () => { + const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } }; + expect(shouldReturnToCart(ConversationState.IDLE, nlu)).toBe(false); + }); + }); + + describe('redirige desde otros estados', () => { + it('redirige add_to_cart desde SHIPPING', () => { + const nlu = { intent: 'add_to_cart', entities: { product_query: 'provoleta' } }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true); + }); + + it('redirige add_to_cart desde PAYMENT', () => { + const nlu = { intent: 'add_to_cart', entities: { product_query: 'vacío' } }; + expect(shouldReturnToCart(ConversationState.PAYMENT, nlu)).toBe(true); + }); + + it('redirige browse desde SHIPPING', () => { + const nlu = { intent: 'browse', entities: { product_query: 'carnes' } }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true); + }); + }); + + describe('no redirige números solos en checkout', () => { + it('no redirige "1" en SHIPPING', () => { + const nlu = { intent: 'add_to_cart', entities: {} }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, '1')).toBe(false); + }); + + it('no redirige "2" en PAYMENT', () => { + const nlu = { intent: 'other', entities: {} }; + expect(shouldReturnToCart(ConversationState.PAYMENT, nlu, '2')).toBe(false); + }); + + it('no redirige "1.5" en SHIPPING', () => { + const nlu = { intent: 'add_to_cart', entities: {} }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, '1.5')).toBe(false); + }); + }); + + describe('requiere producto real', () => { + it('no redirige sin product_query', () => { + const nlu = { intent: 'add_to_cart', entities: {} }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu, 'algo')).toBe(false); + }); + + it('no redirige con product_query muy corto', () => { + const nlu = { intent: 'add_to_cart', entities: { product_query: 'ab' } }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(false); + }); + + it('redirige con items array', () => { + const nlu = { + intent: 'add_to_cart', + entities: { items: [{ product_query: 'provoleta' }] } + }; + expect(shouldReturnToCart(ConversationState.SHIPPING, nlu)).toBe(true); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// deriveNextState +// ───────────────────────────────────────────────────────────── + +describe('deriveNextState', () => { + describe('return_to_cart signal', () => { + it('fuerza CART si return_to_cart', () => { + const result = deriveNextState( + ConversationState.PAYMENT, + {}, + { return_to_cart: true } + ); + expect(result).toBe(ConversationState.CART); + }); + }); + + describe('pagado', () => { + it('va a IDLE si está pagado', () => { + const order = { is_paid: true }; + const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {}); + expect(result).toBe(ConversationState.IDLE); + }); + }); + + describe('esperando pago', () => { + it('va a WAITING_WEBHOOKS si tiene woo_order_id', () => { + const order = { woo_order_id: 123, is_paid: false }; + const result = deriveNextState(ConversationState.PAYMENT, order, {}); + expect(result).toBe(ConversationState.WAITING_WEBHOOKS); + }); + }); + + describe('IDLE -> CART', () => { + it('va a CART si hay cart items', () => { + const order = { cart: [{ woo_id: 1 }], pending: [] }; + const result = deriveNextState(ConversationState.IDLE, order, {}); + expect(result).toBe(ConversationState.CART); + }); + + it('va a CART si hay pending items', () => { + const order = { cart: [], pending: [{ status: 'NEEDS_TYPE' }] }; + const result = deriveNextState(ConversationState.IDLE, order, {}); + expect(result).toBe(ConversationState.CART); + }); + + it('queda en IDLE si vacío', () => { + const order = { cart: [], pending: [] }; + const result = deriveNextState(ConversationState.IDLE, order, {}); + expect(result).toBe(ConversationState.IDLE); + }); + }); + + describe('CART -> SHIPPING', () => { + it('queda en CART si hay pending items', () => { + const order = { cart: [{ woo_id: 1 }], pending: [{ status: 'NEEDS_TYPE' }] }; + const result = deriveNextState(ConversationState.CART, order, { confirm_order: true }); + expect(result).toBe(ConversationState.CART); + }); + + it('va a SHIPPING con confirm_order y cart items', () => { + const order = { cart: [{ woo_id: 1 }], pending: [] }; + const result = deriveNextState(ConversationState.CART, order, { confirm_order: true }); + expect(result).toBe(ConversationState.SHIPPING); + }); + + it('queda en CART sin confirm_order', () => { + const order = { cart: [{ woo_id: 1 }], pending: [] }; + const result = deriveNextState(ConversationState.CART, order, {}); + expect(result).toBe(ConversationState.CART); + }); + }); + + describe('SHIPPING -> PAYMENT', () => { + it('va a PAYMENT con shipping info (pickup)', () => { + const order = { is_delivery: false }; + const result = deriveNextState(ConversationState.SHIPPING, order, {}); + expect(result).toBe(ConversationState.PAYMENT); + }); + + it('va a PAYMENT con shipping info (delivery + address)', () => { + const order = { is_delivery: true, shipping_address: 'Calle 123' }; + const result = deriveNextState(ConversationState.SHIPPING, order, {}); + expect(result).toBe(ConversationState.PAYMENT); + }); + + it('queda en SHIPPING sin info completa', () => { + const order = { is_delivery: true, shipping_address: null }; + const result = deriveNextState(ConversationState.SHIPPING, order, {}); + expect(result).toBe(ConversationState.SHIPPING); + }); + }); + + describe('PAYMENT -> WAITING_WEBHOOKS', () => { + it('va a WAITING con payment_selected', () => { + const order = {}; + const result = deriveNextState( + ConversationState.PAYMENT, + order, + { payment_selected: true } + ); + expect(result).toBe(ConversationState.WAITING_WEBHOOKS); + }); + + it('va a WAITING si ya tiene payment_type', () => { + const order = { payment_type: 'cash' }; + const result = deriveNextState(ConversationState.PAYMENT, order, {}); + expect(result).toBe(ConversationState.WAITING_WEBHOOKS); + }); + }); + + describe('WAITING_WEBHOOKS', () => { + it('va a IDLE si está pagado', () => { + const order = { is_paid: true }; + const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {}); + expect(result).toBe(ConversationState.IDLE); + }); + + it('queda en WAITING si no está pagado', () => { + const order = { is_paid: false }; + const result = deriveNextState(ConversationState.WAITING_WEBHOOKS, order, {}); + expect(result).toBe(ConversationState.WAITING_WEBHOOKS); + }); + }); + + describe('default', () => { + it('retorna IDLE si no hay estado previo', () => { + const result = deriveNextState(null, {}, {}); + expect(result).toBe(ConversationState.IDLE); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// validateTransition +// ───────────────────────────────────────────────────────────── + +describe('validateTransition', () => { + describe('transiciones válidas', () => { + it('IDLE -> IDLE es válido', () => { + const result = validateTransition(ConversationState.IDLE, ConversationState.IDLE); + expect(result.ok).toBe(true); + }); + + it('IDLE -> CART es válido', () => { + const result = validateTransition(ConversationState.IDLE, ConversationState.CART); + expect(result.ok).toBe(true); + }); + + it('CART -> SHIPPING es válido', () => { + const result = validateTransition(ConversationState.CART, ConversationState.SHIPPING); + expect(result.ok).toBe(true); + }); + + it('SHIPPING -> PAYMENT es válido', () => { + const result = validateTransition(ConversationState.SHIPPING, ConversationState.PAYMENT); + expect(result.ok).toBe(true); + }); + + it('PAYMENT -> WAITING_WEBHOOKS es válido', () => { + const result = validateTransition(ConversationState.PAYMENT, ConversationState.WAITING_WEBHOOKS); + expect(result.ok).toBe(true); + }); + + it('SHIPPING -> CART (volver) es válido', () => { + const result = validateTransition(ConversationState.SHIPPING, ConversationState.CART); + expect(result.ok).toBe(true); + }); + }); + + describe('transiciones inválidas', () => { + it('IDLE -> PAYMENT es inválido', () => { + const result = validateTransition(ConversationState.IDLE, ConversationState.PAYMENT); + expect(result.ok).toBe(false); + expect(result.reason).toBe('invalid_transition'); + }); + + it('CART -> WAITING_WEBHOOKS es inválido', () => { + const result = validateTransition(ConversationState.CART, ConversationState.WAITING_WEBHOOKS); + expect(result.ok).toBe(false); + }); + }); + + describe('estados desconocidos', () => { + it('estado previo desconocido', () => { + const result = validateTransition('UNKNOWN', ConversationState.CART); + expect(result.ok).toBe(false); + expect(result.reason).toBe('unknown_prev_state'); + }); + + it('estado siguiente desconocido', () => { + const result = validateTransition(ConversationState.IDLE, 'UNKNOWN'); + expect(result.ok).toBe(false); + expect(result.reason).toBe('unknown_next_state'); + }); + }); + + describe('maneja null/undefined', () => { + it('prevState null se trata como IDLE', () => { + const result = validateTransition(null, ConversationState.CART); + expect(result.ok).toBe(true); + }); + + it('nextState null se trata como IDLE', () => { + const result = validateTransition(ConversationState.IDLE, null); + expect(result.ok).toBe(true); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// safeNextState +// ───────────────────────────────────────────────────────────── + +describe('safeNextState', () => { + it('retorna estado derivado si transición válida', () => { + const order = { cart: [{ woo_id: 1 }], pending: [] }; + const result = safeNextState(ConversationState.CART, order, { confirm_order: true }); + + expect(result.next_state).toBe(ConversationState.SHIPPING); + expect(result.validation.ok).toBe(true); + }); + + it('fuerza CART si transición inválida', () => { + // Forzar una situación donde deriveNextState retornaría un estado inválido + // Esto es difícil de provocar porque deriveNextState ya es bastante seguro + // Pero podemos verificar que la lógica de fallback existe + const order = {}; + const result = safeNextState(ConversationState.IDLE, order, {}); + + // Debería quedarse en IDLE (transición válida) + expect(result.next_state).toBe(ConversationState.IDLE); + expect(result.validation.ok).toBe(true); + }); + + it('incluye validation en resultado', () => { + const order = { is_delivery: false }; + const result = safeNextState(ConversationState.SHIPPING, order, {}); + + expect(result).toHaveProperty('next_state'); + expect(result).toHaveProperty('validation'); + expect(result.validation).toHaveProperty('ok'); + }); + + it('maneja transition IDLE -> CART -> SHIPPING flow', () => { + // Paso 1: IDLE con cart items -> CART + let result = safeNextState(ConversationState.IDLE, { cart: [{ woo_id: 1 }], pending: [] }, {}); + expect(result.next_state).toBe(ConversationState.CART); + + // Paso 2: CART con confirm -> SHIPPING + result = safeNextState(ConversationState.CART, { cart: [{ woo_id: 1 }], pending: [] }, { confirm_order: true }); + expect(result.next_state).toBe(ConversationState.SHIPPING); + + // Paso 3: SHIPPING con pickup -> PAYMENT + result = safeNextState(ConversationState.SHIPPING, { is_delivery: false }, {}); + expect(result.next_state).toBe(ConversationState.PAYMENT); + + // Paso 4: PAYMENT con payment_selected -> WAITING + result = safeNextState(ConversationState.PAYMENT, {}, { payment_selected: true }); + expect(result.next_state).toBe(ConversationState.WAITING_WEBHOOKS); + }); +}); diff --git a/src/modules/3-turn-engine/orderModel.test.js b/src/modules/3-turn-engine/orderModel.test.js new file mode 100644 index 0000000..6f5ff78 --- /dev/null +++ b/src/modules/3-turn-engine/orderModel.test.js @@ -0,0 +1,748 @@ +/** + * Tests para orderModel.js + */ +import { describe, it, expect } from 'vitest'; +import { + PendingStatus, + createEmptyOrder, + createCartItem, + createPendingItem, + moveReadyToCart, + getNextPendingItem, + updatePendingItem, + addPendingItem, + removeCartItem, + updateCartItemQuantity, + migrateOldContext, + formatCartForDisplay, + formatOptionsForDisplay, +} from './orderModel.js'; + +// ───────────────────────────────────────────────────────────── +// createEmptyOrder +// ───────────────────────────────────────────────────────────── + +describe('createEmptyOrder', () => { + it('crea orden con estructura correcta', () => { + const order = createEmptyOrder(); + expect(order).toHaveProperty('cart'); + expect(order).toHaveProperty('pending'); + expect(order).toHaveProperty('payment_type'); + expect(order).toHaveProperty('is_delivery'); + expect(order).toHaveProperty('shipping_address'); + expect(order).toHaveProperty('woo_order_id'); + expect(order).toHaveProperty('is_paid'); + }); + + it('inicializa arrays vacíos', () => { + const order = createEmptyOrder(); + expect(order.cart).toEqual([]); + expect(order.pending).toEqual([]); + }); + + it('inicializa valores null/false', () => { + const order = createEmptyOrder(); + expect(order.payment_type).toBeNull(); + expect(order.is_delivery).toBeNull(); + expect(order.shipping_address).toBeNull(); + expect(order.woo_order_id).toBeNull(); + expect(order.is_paid).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// createCartItem +// ───────────────────────────────────────────────────────────── + +describe('createCartItem', () => { + it('crea item con todos los campos', () => { + const item = createCartItem({ + woo_id: 123, + qty: 2, + unit: 'kg', + name: 'Vacío', + price: 5000, + }); + expect(item.woo_id).toBe(123); + expect(item.qty).toBe(2); + expect(item.unit).toBe('kg'); + expect(item.name).toBe('Vacío'); + expect(item.price).toBe(5000); + }); + + it('usa defaults correctos', () => { + const item = createCartItem({ woo_id: 123 }); + expect(item.qty).toBe(1); + expect(item.unit).toBe('unit'); + expect(item.name).toBeNull(); + expect(item.price).toBeNull(); + }); + + it('convierte qty a número', () => { + const item = createCartItem({ woo_id: 123, qty: '3' }); + expect(item.qty).toBe(3); + }); + + it('usa 1 si qty es inválido', () => { + const item = createCartItem({ woo_id: 123, qty: 'abc' }); + expect(item.qty).toBe(1); + }); +}); + +// ───────────────────────────────────────────────────────────── +// createPendingItem +// ───────────────────────────────────────────────────────────── + +describe('createPendingItem', () => { + it('genera id automático si no se proporciona', () => { + const item = createPendingItem({ query: 'provoleta' }); + expect(item.id).toBeDefined(); + expect(item.id).toMatch(/^pending_/); + }); + + it('usa id proporcionado', () => { + const item = createPendingItem({ id: 'custom_id', query: 'provoleta' }); + expect(item.id).toBe('custom_id'); + }); + + it('usa status NEEDS_TYPE por defecto', () => { + const item = createPendingItem({ query: 'provoleta' }); + expect(item.status).toBe(PendingStatus.NEEDS_TYPE); + }); + + it('acepta status personalizado', () => { + const item = createPendingItem({ + query: 'provoleta', + status: PendingStatus.NEEDS_QUANTITY + }); + expect(item.status).toBe(PendingStatus.NEEDS_QUANTITY); + }); + + it('inicializa campos opcionales a null/[]', () => { + const item = createPendingItem({ query: 'provoleta' }); + expect(item.candidates).toEqual([]); + expect(item.selected_woo_id).toBeNull(); + expect(item.selected_name).toBeNull(); + expect(item.qty).toBeNull(); + }); + + it('guarda requested_qty y requested_unit', () => { + const item = createPendingItem({ + query: 'vacío', + requested_qty: 2, + requested_unit: 'kg' + }); + expect(item.requested_qty).toBe(2); + expect(item.requested_unit).toBe('kg'); + }); +}); + +// ───────────────────────────────────────────────────────────── +// moveReadyToCart +// ───────────────────────────────────────────────────────────── + +describe('moveReadyToCart', () => { + it('mueve items READY al cart', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + id: 'p1', + query: 'provoleta', + status: PendingStatus.READY, + selected_woo_id: 123, + selected_name: 'Provoleta', + qty: 2, + unit: 'unit', + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(1); + expect(result.cart[0].woo_id).toBe(123); + expect(result.cart[0].qty).toBe(2); + expect(result.pending).toHaveLength(0); + }); + + it('ignora items NEEDS_TYPE', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + id: 'p1', + query: 'provoleta', + status: PendingStatus.NEEDS_TYPE, + candidates: [{ woo_id: 1, name: 'A' }], + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(0); + expect(result.pending).toHaveLength(1); + }); + + it('ignora items NEEDS_QUANTITY', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + id: 'p1', + query: 'vacío', + status: PendingStatus.NEEDS_QUANTITY, + selected_woo_id: 123, + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(0); + expect(result.pending).toHaveLength(1); + }); + + it('actualiza item existente en cart', () => { + const order = { + cart: [{ woo_id: 123, qty: 1, unit: 'unit', name: 'Provoleta' }], + pending: [ + createPendingItem({ + id: 'p1', + query: 'provoleta', + status: PendingStatus.READY, + selected_woo_id: 123, + selected_name: 'Provoleta', + qty: 3, + unit: 'unit', + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(1); + expect(result.cart[0].qty).toBe(3); // Actualizado + }); + + it('maneja order null', () => { + const result = moveReadyToCart(null); + expect(result).toEqual(createEmptyOrder()); + }); + + it('no mueve items sin selected_woo_id', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + id: 'p1', + query: 'algo', + status: PendingStatus.READY, + selected_woo_id: null, + qty: 1, + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(0); + expect(result.pending).toHaveLength(1); + }); + + it('no mueve items sin qty', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + id: 'p1', + query: 'algo', + status: PendingStatus.READY, + selected_woo_id: 123, + qty: null, + }), + ], + }; + + const result = moveReadyToCart(order); + expect(result.cart).toHaveLength(0); + expect(result.pending).toHaveLength(1); + }); +}); + +// ───────────────────────────────────────────────────────────── +// getNextPendingItem +// ───────────────────────────────────────────────────────────── + +describe('getNextPendingItem', () => { + it('retorna item NEEDS_TYPE', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.NEEDS_TYPE }), + ], + }; + const next = getNextPendingItem(order); + expect(next).not.toBeNull(); + expect(next.id).toBe('p1'); + }); + + it('retorna item NEEDS_QUANTITY', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.NEEDS_QUANTITY }), + ], + }; + const next = getNextPendingItem(order); + expect(next).not.toBeNull(); + expect(next.id).toBe('p1'); + }); + + it('ignora items READY', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.READY }), + ], + }; + const next = getNextPendingItem(order); + expect(next).toBeNull(); + }); + + it('retorna el primero que necesita clarificación', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'a', status: PendingStatus.READY }), + createPendingItem({ id: 'p2', query: 'b', status: PendingStatus.NEEDS_TYPE }), + createPendingItem({ id: 'p3', query: 'c', status: PendingStatus.NEEDS_QUANTITY }), + ], + }; + const next = getNextPendingItem(order); + expect(next.id).toBe('p2'); // Primero NEEDS_TYPE + }); + + it('retorna null si no hay pending', () => { + expect(getNextPendingItem({ pending: [] })).toBeNull(); + expect(getNextPendingItem({})).toBeNull(); + expect(getNextPendingItem(null)).toBeNull(); + }); +}); + +// ───────────────────────────────────────────────────────────── +// updatePendingItem +// ───────────────────────────────────────────────────────────── + +describe('updatePendingItem', () => { + it('actualiza campos por id', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'provoleta', status: PendingStatus.NEEDS_TYPE }), + ], + }; + + const result = updatePendingItem(order, 'p1', { + status: PendingStatus.READY, + selected_woo_id: 123 + }); + + expect(result.pending[0].status).toBe(PendingStatus.READY); + expect(result.pending[0].selected_woo_id).toBe(123); + expect(result.pending[0].query).toBe('provoleta'); // No modificado + }); + + it('no muta el original', () => { + const original = { + pending: [ + createPendingItem({ id: 'p1', query: 'provoleta', status: PendingStatus.NEEDS_TYPE }), + ], + }; + + updatePendingItem(original, 'p1', { status: PendingStatus.READY }); + + expect(original.pending[0].status).toBe(PendingStatus.NEEDS_TYPE); + }); + + it('no modifica si id no existe', () => { + const order = { + pending: [ + createPendingItem({ id: 'p1', query: 'provoleta' }), + ], + }; + + const result = updatePendingItem(order, 'inexistente', { status: PendingStatus.READY }); + + expect(result.pending[0].status).toBe(PendingStatus.NEEDS_TYPE); + }); + + it('maneja order sin pending', () => { + const result = updatePendingItem({}, 'p1', { status: PendingStatus.READY }); + expect(result).toEqual({}); + }); +}); + +// ───────────────────────────────────────────────────────────── +// addPendingItem +// ───────────────────────────────────────────────────────────── + +describe('addPendingItem', () => { + it('agrega item a pending existente', () => { + const order = { cart: [], pending: [] }; + const newItem = createPendingItem({ query: 'provoleta' }); + + const result = addPendingItem(order, newItem); + + expect(result.pending).toHaveLength(1); + expect(result.pending[0].query).toBe('provoleta'); + }); + + it('preserva items existentes', () => { + const order = { + cart: [], + pending: [createPendingItem({ query: 'vacío' })] + }; + const newItem = createPendingItem({ query: 'provoleta' }); + + const result = addPendingItem(order, newItem); + + expect(result.pending).toHaveLength(2); + }); + + it('crea order si es null', () => { + const newItem = createPendingItem({ query: 'provoleta' }); + + const result = addPendingItem(null, newItem); + + expect(result.cart).toEqual([]); + expect(result.pending).toHaveLength(1); + }); +}); + +// ───────────────────────────────────────────────────────────── +// removeCartItem +// ───────────────────────────────────────────────────────────── + +describe('removeCartItem', () => { + const orderWithItems = { + cart: [ + { woo_id: 1, name: 'Provoleta clásica', qty: 2, unit: 'unit' }, + { woo_id: 2, name: 'Vacío premium', qty: 1.5, unit: 'kg' }, + { woo_id: 3, name: 'Bife de chorizo', qty: 2, unit: 'kg' }, + ], + pending: [], + }; + + it('remueve item por nombre exacto', () => { + const { order, removed } = removeCartItem(orderWithItems, 'vacío premium'); + + expect(removed).not.toBeNull(); + expect(removed.name).toBe('Vacío premium'); + expect(order.cart).toHaveLength(2); + }); + + it('remueve item por palabra clave', () => { + const { order, removed } = removeCartItem(orderWithItems, 'provoleta'); + + expect(removed).not.toBeNull(); + expect(removed.name).toBe('Provoleta clásica'); + }); + + it('remueve item por múltiples palabras', () => { + const { order, removed } = removeCartItem(orderWithItems, 'bife chorizo'); + + expect(removed).not.toBeNull(); + expect(removed.name).toBe('Bife de chorizo'); + }); + + it('retorna null si no encuentra match', () => { + const { order, removed } = removeCartItem(orderWithItems, 'chimichurri'); + + expect(removed).toBeNull(); + expect(order.cart).toHaveLength(3); + }); + + it('maneja cart vacío', () => { + const { order, removed } = removeCartItem({ cart: [] }, 'provoleta'); + + expect(removed).toBeNull(); + }); + + it('maneja order null', () => { + const { order, removed } = removeCartItem(null, 'provoleta'); + + expect(removed).toBeNull(); + }); + + it('maneja productQuery vacío', () => { + const { order, removed } = removeCartItem(orderWithItems, ''); + + expect(removed).toBeNull(); + }); +}); + +// ───────────────────────────────────────────────────────────── +// updateCartItemQuantity +// ───────────────────────────────────────────────────────────── + +describe('updateCartItemQuantity', () => { + const orderWithItems = { + cart: [ + { woo_id: 1, name: 'Vacío premium', qty: 1.5, unit: 'kg' }, + { woo_id: 2, name: 'Provoleta', qty: 2, unit: 'unit' }, + ], + pending: [], + }; + + it('actualiza cantidad', () => { + const { order, updated } = updateCartItemQuantity(orderWithItems, 'vacío', 3); + + expect(updated).not.toBeNull(); + expect(updated.qty).toBe(3); + expect(order.cart.find(i => i.name === 'Vacío premium').qty).toBe(3); + }); + + it('actualiza cantidad y unidad', () => { + const { order, updated } = updateCartItemQuantity(orderWithItems, 'vacío', 500, 'g'); + + expect(updated.qty).toBe(500); + expect(updated.unit).toBe('g'); + }); + + it('retorna null si no encuentra', () => { + const { order, updated } = updateCartItemQuantity(orderWithItems, 'chorizo', 2); + + expect(updated).toBeNull(); + }); + + it('maneja cart vacío', () => { + const { order, updated } = updateCartItemQuantity({ cart: [] }, 'vacío', 2); + + expect(updated).toBeNull(); + }); +}); + +// ───────────────────────────────────────────────────────────── +// migrateOldContext +// ───────────────────────────────────────────────────────────── + +describe('migrateOldContext', () => { + it('retorna orden vacía para ctx null', () => { + const result = migrateOldContext(null); + expect(result).toEqual(createEmptyOrder()); + }); + + it('retorna order existente si ya está migrado', () => { + const ctx = { + order: { + cart: [{ woo_id: 1, qty: 1 }], + pending: [], + }, + }; + + const result = migrateOldContext(ctx); + expect(result.cart).toHaveLength(1); + }); + + it('migra order_basket', () => { + const ctx = { + order_basket: { + items: [ + { product_id: 123, quantity: 2, unit: 'kg', label: 'Vacío', price: 5000 }, + ], + }, + }; + + const result = migrateOldContext(ctx); + expect(result.cart).toHaveLength(1); + expect(result.cart[0].woo_id).toBe(123); + expect(result.cart[0].qty).toBe(2); + expect(result.cart[0].name).toBe('Vacío'); + }); + + it('migra pending_items', () => { + const ctx = { + pending_items: [ + { + id: 'p1', + query: 'provoleta', + status: 'needs_type', + candidates: [{ woo_product_id: 1, name: 'Provoleta A' }], + }, + ], + }; + + const result = migrateOldContext(ctx); + expect(result.pending).toHaveLength(1); + expect(result.pending[0].status).toBe(PendingStatus.NEEDS_TYPE); + }); + + it('migra checkout info', () => { + const ctx = { + payment_method: 'cash', + shipping_method: 'delivery', + delivery_address: { text: 'Calle Falsa 123' }, + woo_order_id: 456, + }; + + const result = migrateOldContext(ctx); + expect(result.payment_type).toBe('cash'); + expect(result.is_delivery).toBe(true); + expect(result.shipping_address).toBe('Calle Falsa 123'); + expect(result.woo_order_id).toBe(456); + }); + + it('migra shipping_method pickup', () => { + const ctx = { + shipping_method: 'pickup', + }; + + const result = migrateOldContext(ctx); + expect(result.is_delivery).toBe(false); + }); +}); + +// ───────────────────────────────────────────────────────────── +// formatCartForDisplay +// ───────────────────────────────────────────────────────────── + +describe('formatCartForDisplay', () => { + it('formatea items con cantidad kg', () => { + const order = { + cart: [ + { woo_id: 1, name: 'Vacío', qty: 1.5, unit: 'kg' }, + ], + pending: [], + }; + + const display = formatCartForDisplay(order); + expect(display).toContain('1.5kg de Vacío'); + }); + + it('formatea items con cantidad g', () => { + const order = { + cart: [ + { woo_id: 1, name: 'Jamón', qty: 500, unit: 'g' }, + ], + pending: [], + }; + + const display = formatCartForDisplay(order); + expect(display).toContain('500g de Jamón'); + }); + + it('formatea items por unidad', () => { + const order = { + cart: [ + { woo_id: 1, name: 'Provoleta', qty: 3, unit: 'unit' }, + ], + pending: [], + }; + + const display = formatCartForDisplay(order); + expect(display).toContain('3 de Provoleta'); + }); + + it('muestra pending items NEEDS_TYPE', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ query: 'provoleta', status: PendingStatus.NEEDS_TYPE }), + ], + }; + + const display = formatCartForDisplay(order); + expect(display).toContain('provoleta'); + expect(display).toContain('pendiente'); + }); + + it('muestra pending items NEEDS_QUANTITY', () => { + const order = { + cart: [], + pending: [ + createPendingItem({ + query: 'vacío', + status: PendingStatus.NEEDS_QUANTITY, + selected_name: 'Vacío premium', + }), + ], + }; + + const display = formatCartForDisplay(order); + expect(display).toContain('Vacío premium'); + expect(display).toContain('falta cantidad'); + }); + + it('muestra mensaje de carrito vacío', () => { + const order = { cart: [], pending: [] }; + + const display = formatCartForDisplay(order); + expect(display).toBe('Tu carrito está vacío.'); + }); + + it('maneja order null', () => { + const display = formatCartForDisplay(null); + expect(display).toBe('Tu carrito está vacío.'); + }); +}); + +// ───────────────────────────────────────────────────────────── +// formatOptionsForDisplay +// ───────────────────────────────────────────────────────────── + +describe('formatOptionsForDisplay', () => { + it('lista opciones numeradas', () => { + const pendingItem = createPendingItem({ + query: 'provoleta', + candidates: [ + { woo_id: 1, name: 'Provoleta clásica' }, + { woo_id: 2, name: 'Provoleta de búfala' }, + ], + }); + + const { question, options } = formatOptionsForDisplay(pendingItem); + + expect(question).toContain('1) Provoleta clásica'); + expect(question).toContain('2) Provoleta de búfala'); + expect(options).toHaveLength(2); + }); + + it('muestra "Mostrame más" si hay más de pageSize', () => { + const candidates = Array.from({ length: 15 }, (_, i) => ({ + woo_id: i, + name: `Producto ${i}`, + })); + const pendingItem = createPendingItem({ + query: 'producto', + candidates, + }); + + const { question } = formatOptionsForDisplay(pendingItem, 12); + + expect(question).toContain('13) Mostrame más'); + }); + + it('no muestra "Mostrame más" si hay menos de pageSize', () => { + const pendingItem = createPendingItem({ + query: 'provoleta', + candidates: [ + { woo_id: 1, name: 'A' }, + { woo_id: 2, name: 'B' }, + ], + }); + + const { question } = formatOptionsForDisplay(pendingItem); + + expect(question).not.toContain('Mostrame más'); + }); + + it('muestra mensaje de no encontrado sin candidatos', () => { + const pendingItem = createPendingItem({ + query: 'xyz', + candidates: [], + }); + + const { question, options } = formatOptionsForDisplay(pendingItem); + + expect(question).toContain('No encontré'); + expect(question).toContain('xyz'); + expect(options).toEqual([]); + }); + + it('maneja pendingItem null', () => { + const { question } = formatOptionsForDisplay(null); + expect(question).toContain('No encontré'); + }); +}); diff --git a/src/modules/3-turn-engine/stateHandlers/utils.test.js b/src/modules/3-turn-engine/stateHandlers/utils.test.js new file mode 100644 index 0000000..09c0878 --- /dev/null +++ b/src/modules/3-turn-engine/stateHandlers/utils.test.js @@ -0,0 +1,448 @@ +/** + * Tests para utils.js + */ +import { describe, it, expect } from 'vitest'; +import { + inferDefaultUnit, + parseIndexSelection, + isShowMoreRequest, + isShowOptionsRequest, + findMatchingCandidate, + isEscapeRequest, + normalizeUnit, + unitAskFor, +} from './utils.js'; + +// ───────────────────────────────────────────────────────────── +// parseIndexSelection +// ───────────────────────────────────────────────────────────── + +describe('parseIndexSelection', () => { + describe('números directos', () => { + it('parsea número simple', () => { + expect(parseIndexSelection('2')).toBe(2); + expect(parseIndexSelection('5')).toBe(5); + expect(parseIndexSelection('10')).toBe(10); + }); + + it('parsea número en frase', () => { + expect(parseIndexSelection('quiero el 2')).toBe(2); + expect(parseIndexSelection('dame la opción 3')).toBe(3); + expect(parseIndexSelection('el número 7 por favor')).toBe(7); + }); + + it('parsea números de dos dígitos', () => { + expect(parseIndexSelection('el 12')).toBe(12); + expect(parseIndexSelection('opción 15')).toBe(15); + }); + }); + + describe('ordinales en español', () => { + it('parsea ordinales masculinos', () => { + expect(parseIndexSelection('el primero')).toBe(1); + expect(parseIndexSelection('segundo')).toBe(2); + expect(parseIndexSelection('tercero')).toBe(3); + expect(parseIndexSelection('cuarto')).toBe(4); + expect(parseIndexSelection('quinto')).toBe(5); + expect(parseIndexSelection('sexto')).toBe(6); + expect(parseIndexSelection('séptimo')).toBe(7); + expect(parseIndexSelection('octavo')).toBe(8); + expect(parseIndexSelection('noveno')).toBe(9); + expect(parseIndexSelection('décimo')).toBe(10); + }); + + it('parsea ordinales femeninos', () => { + expect(parseIndexSelection('la primera')).toBe(1); + expect(parseIndexSelection('segunda')).toBe(2); + expect(parseIndexSelection('tercera')).toBe(3); + expect(parseIndexSelection('cuarta')).toBe(4); + expect(parseIndexSelection('quinta')).toBe(5); + expect(parseIndexSelection('sexta')).toBe(6); + expect(parseIndexSelection('séptima')).toBe(7); + expect(parseIndexSelection('octava')).toBe(8); + expect(parseIndexSelection('novena')).toBe(9); + expect(parseIndexSelection('décima')).toBe(10); + }); + + it('parsea ordinales sin tilde', () => { + expect(parseIndexSelection('septimo')).toBe(7); + expect(parseIndexSelection('decimo')).toBe(10); + }); + }); + + describe('casos sin selección', () => { + it('retorna null para texto sin número ni ordinal', () => { + expect(parseIndexSelection('hola')).toBeNull(); + expect(parseIndexSelection('quiero provoleta')).toBeNull(); + expect(parseIndexSelection('no sé')).toBeNull(); + }); + + it('retorna null para valores vacíos', () => { + expect(parseIndexSelection('')).toBeNull(); + expect(parseIndexSelection(null)).toBeNull(); + expect(parseIndexSelection(undefined)).toBeNull(); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// normalizeUnit +// ───────────────────────────────────────────────────────────── + +describe('normalizeUnit', () => { + describe('kilogramos', () => { + it('normaliza kg', () => { + expect(normalizeUnit('kg')).toBe('kg'); + expect(normalizeUnit('KG')).toBe('kg'); + }); + + it('normaliza kilo/kilos', () => { + expect(normalizeUnit('kilo')).toBe('kg'); + expect(normalizeUnit('kilos')).toBe('kg'); + expect(normalizeUnit('KILOS')).toBe('kg'); + }); + }); + + describe('gramos', () => { + it('normaliza g', () => { + expect(normalizeUnit('g')).toBe('g'); + expect(normalizeUnit('G')).toBe('g'); + }); + + it('normaliza gramo/gramos', () => { + expect(normalizeUnit('gramo')).toBe('g'); + expect(normalizeUnit('gramos')).toBe('g'); + expect(normalizeUnit('GRAMOS')).toBe('g'); + }); + }); + + describe('unidades', () => { + it('normaliza unit', () => { + expect(normalizeUnit('unit')).toBe('unit'); + }); + + it('normaliza unidad/unidades', () => { + expect(normalizeUnit('unidad')).toBe('unit'); + expect(normalizeUnit('unidades')).toBe('unit'); + expect(normalizeUnit('UNIDADES')).toBe('unit'); + }); + }); + + describe('valores inválidos', () => { + it('retorna null para unidades desconocidas', () => { + expect(normalizeUnit('litro')).toBeNull(); + expect(normalizeUnit('docena')).toBeNull(); + expect(normalizeUnit('xyz')).toBeNull(); + }); + + it('retorna null para valores vacíos', () => { + expect(normalizeUnit('')).toBeNull(); + expect(normalizeUnit(null)).toBeNull(); + expect(normalizeUnit(undefined)).toBeNull(); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// inferDefaultUnit +// ───────────────────────────────────────────────────────────── + +describe('inferDefaultUnit', () => { + describe('productos que se venden por unidad', () => { + it('detecta provoleta/queso por nombre', () => { + expect(inferDefaultUnit({ name: 'Provoleta clásica', categories: [] })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Queso provolone', categories: [] })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Pan de campo', categories: [] })).toBe('unit'); + }); + + it('detecta bebidas por nombre', () => { + expect(inferDefaultUnit({ name: 'Vino Malbec', categories: [] })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Cerveza artesanal', categories: [] })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Fernet Branca', categories: [] })).toBe('unit'); + }); + + it('detecta condimentos', () => { + expect(inferDefaultUnit({ name: 'Chimichurri casero', categories: [] })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Salsa criolla', categories: [] })).toBe('unit'); + }); + + it('detecta por categoría', () => { + expect(inferDefaultUnit({ + name: 'Producto X', + categories: [{ name: 'Vinos', slug: 'vinos' }] + })).toBe('unit'); + expect(inferDefaultUnit({ + name: 'Producto Y', + categories: [{ name: 'Proveeduría', slug: 'proveeduria' }] + })).toBe('unit'); + }); + }); + + describe('productos que se venden por kg', () => { + it('retorna kg para carnes', () => { + expect(inferDefaultUnit({ name: 'Bife de chorizo', categories: [] })).toBe('kg'); + expect(inferDefaultUnit({ name: 'Vacío', categories: [] })).toBe('kg'); + expect(inferDefaultUnit({ name: 'Asado de tira', categories: [] })).toBe('kg'); + }); + + it('retorna kg por defecto', () => { + expect(inferDefaultUnit({ name: 'Producto genérico', categories: [] })).toBe('kg'); + expect(inferDefaultUnit({ name: '', categories: [] })).toBe('kg'); + }); + }); + + describe('edge cases', () => { + it('maneja categories no array', () => { + expect(inferDefaultUnit({ name: 'Vino', categories: null })).toBe('unit'); + expect(inferDefaultUnit({ name: 'Vino', categories: undefined })).toBe('unit'); + }); + + it('maneja name vacío o null', () => { + expect(inferDefaultUnit({ name: null, categories: [] })).toBe('kg'); + expect(inferDefaultUnit({ name: undefined, categories: [] })).toBe('kg'); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// isShowMoreRequest +// ───────────────────────────────────────────────────────────── + +describe('isShowMoreRequest', () => { + describe('detecta pedidos de más opciones', () => { + it('detecta "mostrame más"', () => { + expect(isShowMoreRequest('mostrame más')).toBe(true); + expect(isShowMoreRequest('mostrame mas')).toBe(true); + expect(isShowMoreRequest('mostra más')).toBe(true); + }); + + it('detecta "más opciones"', () => { + expect(isShowMoreRequest('más opciones')).toBe(true); + expect(isShowMoreRequest('mas opciones')).toBe(true); + expect(isShowMoreRequest('quiero más opciones')).toBe(true); + }); + + it('detecta "siguientes"', () => { + expect(isShowMoreRequest('siguientes')).toBe(true); + expect(isShowMoreRequest('siguiente')).toBe(true); + }); + + it('detecta "otras opciones"', () => { + expect(isShowMoreRequest('otras opciones')).toBe(true); + expect(isShowMoreRequest('hay otras?')).toBe(true); + }); + + it('detecta "ver más"', () => { + expect(isShowMoreRequest('ver más')).toBe(true); + expect(isShowMoreRequest('ver mas')).toBe(true); + }); + + it('detecta "qué más hay"', () => { + expect(isShowMoreRequest('qué más hay')).toBe(true); + expect(isShowMoreRequest('que mas hay')).toBe(true); + }); + }); + + describe('no detecta falsos positivos', () => { + it('no detecta frases normales', () => { + expect(isShowMoreRequest('quiero el primero')).toBe(false); + expect(isShowMoreRequest('dame provoleta')).toBe(false); + expect(isShowMoreRequest('hola')).toBe(false); + }); + + it('no detecta valores vacíos', () => { + expect(isShowMoreRequest('')).toBe(false); + expect(isShowMoreRequest(null)).toBe(false); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// isShowOptionsRequest +// ───────────────────────────────────────────────────────────── + +describe('isShowOptionsRequest', () => { + describe('detecta pedidos de ver opciones', () => { + it('detecta "qué opciones"', () => { + expect(isShowOptionsRequest('qué opciones tenés?')).toBe(true); + expect(isShowOptionsRequest('que opciones hay')).toBe(true); + }); + + it('detecta "cuáles tenés"', () => { + expect(isShowOptionsRequest('cuáles tenés')).toBe(true); + expect(isShowOptionsRequest('cuales son')).toBe(true); + expect(isShowOptionsRequest('cuáles hay')).toBe(true); + }); + + it('detecta "qué hay"', () => { + expect(isShowOptionsRequest('qué hay')).toBe(true); + expect(isShowOptionsRequest('que hay')).toBe(true); + }); + + it('detecta "qué tenés"', () => { + expect(isShowOptionsRequest('qué tenés')).toBe(true); + expect(isShowOptionsRequest('que tenes')).toBe(true); + }); + + it('detecta "ver opciones"', () => { + expect(isShowOptionsRequest('ver opciones')).toBe(true); + expect(isShowOptionsRequest('ver las opciones')).toBe(true); + }); + }); + + describe('no detecta falsos positivos', () => { + it('no detecta frases normales', () => { + expect(isShowOptionsRequest('quiero el 2')).toBe(false); + expect(isShowOptionsRequest('dame provoleta')).toBe(false); + }); + + it('no detecta valores vacíos', () => { + expect(isShowOptionsRequest('')).toBe(false); + expect(isShowOptionsRequest(null)).toBe(false); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// isEscapeRequest +// ───────────────────────────────────────────────────────────── + +describe('isEscapeRequest', () => { + describe('detecta pedidos de escape', () => { + it('detecta "qué tengo"', () => { + expect(isEscapeRequest('qué tengo')).toBe(true); + expect(isEscapeRequest('que tengo en el carrito')).toBe(true); + }); + + it('detecta "mi carrito"', () => { + expect(isEscapeRequest('mi carrito')).toBe(true); + expect(isEscapeRequest('mi pedido')).toBe(true); + }); + + it('detecta "ver carrito"', () => { + expect(isEscapeRequest('ver carrito')).toBe(true); + expect(isEscapeRequest('ver pedido')).toBe(true); + }); + + it('detecta "listo"', () => { + expect(isEscapeRequest('listo')).toBe(true); + expect(isEscapeRequest('ya está listo')).toBe(true); + }); + + it('detecta "confirmar"', () => { + expect(isEscapeRequest('confirmar')).toBe(true); + expect(isEscapeRequest('quiero confirmar')).toBe(true); + }); + + it('detecta "cancelar"', () => { + expect(isEscapeRequest('cancelar')).toBe(true); + expect(isEscapeRequest('quiero cancelar')).toBe(true); + }); + + it('detecta "eso es todo"', () => { + expect(isEscapeRequest('eso es todo')).toBe(true); + expect(isEscapeRequest('eso todo')).toBe(true); + }); + }); + + describe('no detecta falsos positivos', () => { + it('no detecta productos', () => { + expect(isEscapeRequest('quiero provoleta')).toBe(false); + expect(isEscapeRequest('dame 2kg de vacío')).toBe(false); + }); + + it('no detecta valores vacíos', () => { + expect(isEscapeRequest('')).toBe(false); + expect(isEscapeRequest(null)).toBe(false); + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// findMatchingCandidate +// ───────────────────────────────────────────────────────────── + +describe('findMatchingCandidate', () => { + const candidates = [ + { name: 'Provoleta de bufala' }, + { name: 'Provoleta clasica' }, + { name: 'Queso provolone' }, + { name: 'Chimichurri casero' }, + ]; + + describe('encuentra matches', () => { + it('encuentra match exacto de palabra', () => { + const match = findMatchingCandidate(candidates, 'bufala'); + expect(match).not.toBeNull(); + expect(match.index).toBe(0); + expect(match.candidate.name).toBe('Provoleta de bufala'); + }); + + it('encuentra match parcial', () => { + const match = findMatchingCandidate(candidates, 'clasica'); + expect(match).not.toBeNull(); + expect(match.index).toBe(1); + }); + + it('da bonus por match completo', () => { + const match = findMatchingCandidate(candidates, 'provoleta clasica'); + expect(match).not.toBeNull(); + expect(match.index).toBe(1); + expect(match.score).toBeGreaterThan(1); + }); + + it('encuentra mejor match entre varios', () => { + const match = findMatchingCandidate(candidates, 'provoleta'); + expect(match).not.toBeNull(); + // Ambos tienen "provoleta", debe elegir uno + expect([0, 1]).toContain(match.index); + }); + }); + + describe('no encuentra match', () => { + it('retorna null sin coincidencia', () => { + expect(findMatchingCandidate(candidates, 'chorizo')).toBeNull(); + expect(findMatchingCandidate(candidates, 'vino')).toBeNull(); + }); + + it('retorna null para candidates vacío', () => { + expect(findMatchingCandidate([], 'provoleta')).toBeNull(); + expect(findMatchingCandidate(null, 'provoleta')).toBeNull(); + }); + + it('retorna null para texto vacío', () => { + expect(findMatchingCandidate(candidates, '')).toBeNull(); + expect(findMatchingCandidate(candidates, null)).toBeNull(); + }); + + it('texto corto puede matchear por inclusión completa', () => { + // "de" tiene 2 caracteres, se filtra como palabra pero si el texto completo + // está en el nombre, da bonus y matchea + const match = findMatchingCandidate(candidates, 'de'); + // La función da bonus si el texto completo está en el nombre + expect(match).not.toBeNull(); + expect(match.score).toBe(2); // bonus por match completo + }); + }); +}); + +// ───────────────────────────────────────────────────────────── +// unitAskFor +// ───────────────────────────────────────────────────────────── + +describe('unitAskFor', () => { + it('genera pregunta para unidades', () => { + expect(unitAskFor('unit')).toBe('¿Cuántas unidades querés?'); + }); + + it('genera pregunta para gramos', () => { + expect(unitAskFor('g')).toBe('¿Cuántos gramos querés?'); + }); + + it('genera pregunta para kilos (default)', () => { + expect(unitAskFor('kg')).toBe('¿Cuántos kilos querés?'); + expect(unitAskFor(null)).toBe('¿Cuántos kilos querés?'); + expect(unitAskFor(undefined)).toBe('¿Cuántos kilos querés?'); + expect(unitAskFor('otro')).toBe('¿Cuántos kilos querés?'); + }); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..745de5b --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['**/*.test.js'], + coverage: { + provider: 'v8', + reporter: ['text', 'html'], + }, + }, +})