diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..7125472b1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3256 @@ +{ + "name": "gridpilot", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gridpilot", + "version": "0.1.0", + "workspaces": [ + "src/packages/*", + "src/apps/*" + ], + "devDependencies": { + "@cucumber/cucumber": "^11.0.1", + "@types/node": "^22.10.2", + "@vitest/ui": "^2.1.8", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "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/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/ci-environment": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", + "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/cucumber": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-11.3.0.tgz", + "integrity": "sha512-1YGsoAzRfDyVOnRMTSZP/EcFsOBElOKa2r+5nin0DJAeK+Mp0mzjcmSllMgApGtck7Ji87wwy3kFONfHUHMn4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/ci-environment": "10.0.1", + "@cucumber/cucumber-expressions": "18.0.1", + "@cucumber/gherkin": "30.0.4", + "@cucumber/gherkin-streams": "5.0.1", + "@cucumber/gherkin-utils": "9.2.0", + "@cucumber/html-formatter": "21.10.1", + "@cucumber/junit-xml-formatter": "0.7.1", + "@cucumber/message-streams": "4.0.1", + "@cucumber/messages": "27.2.0", + "@cucumber/tag-expressions": "6.1.2", + "assertion-error-formatter": "^3.0.0", + "capital-case": "^1.0.4", + "chalk": "^4.1.2", + "cli-table3": "0.6.5", + "commander": "^10.0.0", + "debug": "^4.3.4", + "error-stack-parser": "^2.1.4", + "figures": "^3.2.0", + "glob": "^10.3.10", + "has-ansi": "^4.0.1", + "indent-string": "^4.0.0", + "is-installed-globally": "^0.4.0", + "is-stream": "^2.0.0", + "knuth-shuffle-seeded": "^1.0.6", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", + "luxon": "3.6.1", + "mime": "^3.0.0", + "mkdirp": "^2.1.5", + "mz": "^2.7.0", + "progress": "^2.0.3", + "read-package-up": "^11.0.0", + "semver": "7.7.1", + "string-argv": "0.3.1", + "supports-color": "^8.1.1", + "type-fest": "^4.41.0", + "util-arity": "^1.1.0", + "yaml": "^2.2.2", + "yup": "1.6.1" + }, + "bin": { + "cucumber-js": "bin/cucumber.js" + }, + "engines": { + "node": "18 || 20 || 22 || >=23" + }, + "funding": { + "url": "https://opencollective.com/cucumber" + } + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-18.0.1.tgz", + "integrity": "sha512-NSid6bI+7UlgMywl5octojY5NXnxR9uq+JisjOrO52VbFsQM6gTWuQFE8syI10KnIBEdPzuEUSVEeZ0VFzRnZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-30.0.4.tgz", + "integrity": "sha512-pb7lmAJqweZRADTTsgnC3F5zbTh3nwOB1M83Q9ZPbUKMb3P76PzK6cTcPTJBHWy3l7isbigIv+BkDjaca6C8/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <=26" + } + }, + "node_modules/@cucumber/gherkin-streams": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", + "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "9.1.0", + "source-map-support": "0.5.21" + }, + "bin": { + "gherkin-javascript": "bin/gherkin" + }, + "peerDependencies": { + "@cucumber/gherkin": ">=22.0.0", + "@cucumber/message-streams": ">=4.0.0", + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/gherkin-streams/node_modules/commander": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", + "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", + "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^31.0.0", + "@cucumber/messages": "^27.0.0", + "@teppeis/multimaps": "3.0.0", + "commander": "13.1.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", + "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=19.1.4 <=26" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", + "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "10.0.0" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cucumber/gherkin/node_modules/@cucumber/messages": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", + "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "10.0.0" + } + }, + "node_modules/@cucumber/gherkin/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cucumber/html-formatter": { + "version": "21.10.1", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.10.1.tgz", + "integrity": "sha512-isaaNMNnBYThsvaHy7i+9kkk9V3+rhgdkt0pd6TCY6zY1CSRZQ7tG6ST9pYyRaECyfbCeF7UGH0KpNEnh6UNvQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/junit-xml-formatter": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.7.1.tgz", + "integrity": "sha512-AzhX+xFE/3zfoYeqkT7DNq68wAQfBcx4Dk9qS/ocXM2v5tBv6eFQ+w8zaSfsktCjYzu4oYRH/jh4USD1CYHfaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/query": "^13.0.2", + "@teppeis/multimaps": "^3.0.0", + "luxon": "^3.5.0", + "xmlbuilder": "^15.1.1" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/message-streams": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", + "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=17.1.1" + } + }, + "node_modules/@cucumber/messages": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-27.2.0.tgz", + "integrity": "sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "10.0.0", + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2", + "uuid": "11.0.5" + } + }, + "node_modules/@cucumber/query": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", + "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.1.2.tgz", + "integrity": "sha512-xa3pER+ntZhGCxRXSguDTKEHTZpUUsp+RzTRNnit+vi5cqnk6abLdSLg5i3HZXU3c74nQ8afQC6IT507EN74oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@teppeis/multimaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", + "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.9.tgz", + "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.10", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.9" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "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/assertion-error-formatter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", + "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff": "^4.0.1", + "pad-right": "^0.2.2", + "repeat-string": "^1.6.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/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/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "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/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-ansi": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-4.0.1.tgz", + "integrity": "sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/knuth-shuffle-seeded": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", + "integrity": "sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "seed-random": "~2.2.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/luxon": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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": "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/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/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "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.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "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/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true, + "license": "MIT" + }, + "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/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "dev": true, + "license": "MIT" + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/util-arity": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", + "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yup": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 8dbadf8b5..cfab26b9c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { + "@cucumber/cucumber": "^11.0.1", "@types/node": "^22.10.2", + "@vitest/ui": "^2.1.8", "typescript": "^5.7.2", "vitest": "^2.1.8" } diff --git a/src/infrastructure/adapters/automation/MockBrowserAutomationAdapter.ts b/src/infrastructure/adapters/automation/MockBrowserAutomationAdapter.ts new file mode 100644 index 000000000..5eebf9845 --- /dev/null +++ b/src/infrastructure/adapters/automation/MockBrowserAutomationAdapter.ts @@ -0,0 +1,172 @@ +import { StepId } from '../../../packages/domain/value-objects/StepId'; +import { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig'; +import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation'; + +interface MockConfig { + simulateFailures?: boolean; + failureRate?: number; +} + +interface StepExecutionResult { + success: boolean; + stepId: number; + wasModalStep?: boolean; + shouldStop?: boolean; + executionTime: number; + metrics: { + totalDelay: number; + operationCount: number; + }; +} + +interface NavigationResult { + success: boolean; + url: string; + simulatedDelay: number; +} + +interface FormFillResult { + success: boolean; + fieldName: string; + value: string; + simulatedDelay: number; +} + +interface ClickResult { + success: boolean; + selector: string; + simulatedDelay: number; +} + +interface WaitResult { + success: boolean; + selector: string; + simulatedDelay: number; +} + +interface ModalResult { + success: boolean; + stepId: number; + action: string; + simulatedDelay: number; +} + +export class MockBrowserAutomationAdapter implements IBrowserAutomation { + private config: MockConfig; + + constructor(config: MockConfig = {}) { + this.config = { + simulateFailures: config.simulateFailures ?? false, + failureRate: config.failureRate ?? 0.1, + }; + } + + async navigateToPage(url: string): Promise { + const delay = this.randomDelay(200, 800); + await this.sleep(delay); + return { + success: true, + url, + simulatedDelay: delay, + }; + } + + async fillFormField(fieldName: string, value: string): Promise { + const delay = this.randomDelay(100, 500); + await this.sleep(delay); + return { + success: true, + fieldName, + value, + simulatedDelay: delay, + }; + } + + async clickElement(selector: string): Promise { + const delay = this.randomDelay(50, 300); + await this.sleep(delay); + return { + success: true, + selector, + simulatedDelay: delay, + }; + } + + async waitForElement(selector: string, maxWaitMs: number = 5000): Promise { + const delay = this.randomDelay(100, 1000); + + await this.sleep(delay); + + return { + success: true, + selector, + simulatedDelay: delay, + }; + } + + async handleModal(stepId: StepId, action: string): Promise { + if (!stepId.isModalStep()) { + throw new Error(`Step ${stepId.value} is not a modal step`); + } + + const delay = this.randomDelay(200, 600); + await this.sleep(delay); + return { + success: true, + stepId: stepId.value, + action, + simulatedDelay: delay, + }; + } + + async executeStep(stepId: StepId, config: HostedSessionConfig): Promise { + if (this.shouldSimulateFailure()) { + throw new Error(`Simulated failure at step ${stepId.value}`); + } + + const startTime = Date.now(); + let totalDelay = 0; + let operationCount = 0; + + const navigationDelay = this.randomDelay(200, 500); + await this.sleep(navigationDelay); + totalDelay += navigationDelay; + operationCount++; + + if (stepId.isModalStep()) { + const modalDelay = this.randomDelay(200, 400); + await this.sleep(modalDelay); + totalDelay += modalDelay; + operationCount++; + } + + const executionTime = Date.now() - startTime; + + return { + success: true, + stepId: stepId.value, + wasModalStep: stepId.isModalStep(), + shouldStop: stepId.isFinalStep(), + executionTime, + metrics: { + totalDelay, + operationCount, + }, + }; + } + + private randomDelay(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + private async sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private shouldSimulateFailure(): boolean { + if (!this.config.simulateFailures) { + return false; + } + return Math.random() < (this.config.failureRate || 0.1); + } +} \ No newline at end of file diff --git a/src/infrastructure/repositories/InMemorySessionRepository.ts b/src/infrastructure/repositories/InMemorySessionRepository.ts new file mode 100644 index 000000000..ee6792d54 --- /dev/null +++ b/src/infrastructure/repositories/InMemorySessionRepository.ts @@ -0,0 +1,36 @@ +import { AutomationSession } from '../../packages/domain/entities/AutomationSession'; +import { SessionStateValue } from '../../packages/domain/value-objects/SessionState'; +import { ISessionRepository } from '../../packages/application/ports/ISessionRepository'; + +export class InMemorySessionRepository implements ISessionRepository { + private sessions: Map = new Map(); + + async save(session: AutomationSession): Promise { + this.sessions.set(session.id, session); + } + + async findById(id: string): Promise { + return this.sessions.get(id) || null; + } + + async update(session: AutomationSession): Promise { + if (!this.sessions.has(session.id)) { + throw new Error('Session not found'); + } + this.sessions.set(session.id, session); + } + + async delete(id: string): Promise { + this.sessions.delete(id); + } + + async findAll(): Promise { + return Array.from(this.sessions.values()); + } + + async findByState(state: SessionStateValue): Promise { + return Array.from(this.sessions.values()).filter( + session => session.state.value === state + ); + } +} \ No newline at end of file diff --git a/src/packages/application/ports/IAutomationEngine.ts b/src/packages/application/ports/IAutomationEngine.ts new file mode 100644 index 000000000..5d54f59af --- /dev/null +++ b/src/packages/application/ports/IAutomationEngine.ts @@ -0,0 +1,12 @@ +import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig'; +import { StepId } from '../../domain/value-objects/StepId'; + +export interface ValidationResult { + isValid: boolean; + error?: string; +} + +export interface IAutomationEngine { + validateConfiguration(config: HostedSessionConfig): Promise; + executeStep(stepId: StepId, config: HostedSessionConfig): Promise; +} \ No newline at end of file diff --git a/src/packages/application/ports/IBrowserAutomation.ts b/src/packages/application/ports/IBrowserAutomation.ts new file mode 100644 index 000000000..531574bb2 --- /dev/null +++ b/src/packages/application/ports/IBrowserAutomation.ts @@ -0,0 +1,9 @@ +import { StepId } from '../../domain/value-objects/StepId'; + +export interface IBrowserAutomation { + navigateToPage(url: string): Promise; + fillFormField(fieldName: string, value: string): Promise; + clickElement(selector: string): Promise; + waitForElement(selector: string, maxWaitMs?: number): Promise; + handleModal(stepId: StepId, action: string): Promise; +} \ No newline at end of file diff --git a/src/packages/application/ports/ISessionRepository.ts b/src/packages/application/ports/ISessionRepository.ts new file mode 100644 index 000000000..64f6689cd --- /dev/null +++ b/src/packages/application/ports/ISessionRepository.ts @@ -0,0 +1,11 @@ +import { AutomationSession } from '../../domain/entities/AutomationSession'; +import { SessionStateValue } from '../../domain/value-objects/SessionState'; + +export interface ISessionRepository { + save(session: AutomationSession): Promise; + findById(id: string): Promise; + update(session: AutomationSession): Promise; + delete(id: string): Promise; + findAll(): Promise; + findByState(state: SessionStateValue): Promise; +} \ No newline at end of file diff --git a/src/packages/application/use-cases/StartAutomationSessionUseCase.ts b/src/packages/application/use-cases/StartAutomationSessionUseCase.ts new file mode 100644 index 000000000..fdd297d99 --- /dev/null +++ b/src/packages/application/use-cases/StartAutomationSessionUseCase.ts @@ -0,0 +1,44 @@ +import { AutomationSession } from '../../domain/entities/AutomationSession'; +import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig'; +import { IAutomationEngine } from '../ports/IAutomationEngine'; +import { IBrowserAutomation } from '../ports/IBrowserAutomation'; +import { ISessionRepository } from '../ports/ISessionRepository'; + +export interface SessionDTO { + sessionId: string; + state: string; + currentStep: number; + config: HostedSessionConfig; + startedAt?: Date; + completedAt?: Date; + errorMessage?: string; +} + +export class StartAutomationSessionUseCase { + constructor( + private readonly automationEngine: IAutomationEngine, + private readonly browserAutomation: IBrowserAutomation, + private readonly sessionRepository: ISessionRepository + ) {} + + async execute(config: HostedSessionConfig): Promise { + const session = AutomationSession.create(config); + + const validationResult = await this.automationEngine.validateConfiguration(config); + if (!validationResult.isValid) { + throw new Error(validationResult.error); + } + + await this.sessionRepository.save(session); + + return { + sessionId: session.id, + state: session.state.value, + currentStep: session.currentStep.value, + config: session.config, + startedAt: session.startedAt, + completedAt: session.completedAt, + errorMessage: session.errorMessage, + }; + } +} \ No newline at end of file diff --git a/src/packages/domain/entities/AutomationSession.ts b/src/packages/domain/entities/AutomationSession.ts new file mode 100644 index 000000000..31db664de --- /dev/null +++ b/src/packages/domain/entities/AutomationSession.ts @@ -0,0 +1,143 @@ +import { randomUUID } from 'crypto'; +import { StepId } from '../value-objects/StepId'; +import { SessionState } from '../value-objects/SessionState'; +import { HostedSessionConfig } from './HostedSessionConfig'; + +export class AutomationSession { + private readonly _id: string; + private _currentStep: StepId; + private _state: SessionState; + private readonly _config: HostedSessionConfig; + private _startedAt?: Date; + private _completedAt?: Date; + private _errorMessage?: string; + + private constructor( + id: string, + currentStep: StepId, + state: SessionState, + config: HostedSessionConfig + ) { + this._id = id; + this._currentStep = currentStep; + this._state = state; + this._config = config; + } + + static create(config: HostedSessionConfig): AutomationSession { + if (!config.sessionName || config.sessionName.trim() === '') { + throw new Error('Session name cannot be empty'); + } + if (!config.trackId || config.trackId.trim() === '') { + throw new Error('Track ID is required'); + } + if (!config.carIds || config.carIds.length === 0) { + throw new Error('At least one car must be selected'); + } + + return new AutomationSession( + randomUUID(), + StepId.create(1), + SessionState.create('PENDING'), + config + ); + } + + get id(): string { + return this._id; + } + + get currentStep(): StepId { + return this._currentStep; + } + + get state(): SessionState { + return this._state; + } + + get config(): HostedSessionConfig { + return this._config; + } + + get startedAt(): Date | undefined { + return this._startedAt; + } + + get completedAt(): Date | undefined { + return this._completedAt; + } + + get errorMessage(): string | undefined { + return this._errorMessage; + } + + start(): void { + if (!this._state.isPending()) { + throw new Error('Cannot start session that is not pending'); + } + this._state = SessionState.create('IN_PROGRESS'); + this._startedAt = new Date(); + } + + transitionToStep(targetStep: StepId): void { + if (!this._state.isInProgress()) { + throw new Error('Cannot transition steps when session is not in progress'); + } + + if (this._currentStep.equals(targetStep)) { + throw new Error('Already at this step'); + } + + if (targetStep.value < this._currentStep.value) { + throw new Error('Cannot move backward - steps must progress forward only'); + } + + if (targetStep.value !== this._currentStep.value + 1) { + throw new Error('Cannot skip steps - must transition sequentially'); + } + + this._currentStep = targetStep; + + if (this._currentStep.isFinalStep()) { + this._state = SessionState.create('STOPPED_AT_STEP_18'); + this._completedAt = new Date(); + } + } + + pause(): void { + if (!this._state.isInProgress()) { + throw new Error('Cannot pause session that is not in progress'); + } + this._state = SessionState.create('PAUSED'); + } + + resume(): void { + if (this._state.value !== 'PAUSED') { + throw new Error('Cannot resume session that is not paused'); + } + this._state = SessionState.create('IN_PROGRESS'); + } + + fail(errorMessage: string): void { + if (this._state.isTerminal()) { + throw new Error('Cannot fail terminal session'); + } + this._state = SessionState.create('FAILED'); + this._errorMessage = errorMessage; + this._completedAt = new Date(); + } + + isAtModalStep(): boolean { + return this._currentStep.isModalStep(); + } + + getElapsedTime(): number { + if (!this._startedAt) { + return 0; + } + + const endTime = this._completedAt || new Date(); + const elapsed = endTime.getTime() - this._startedAt.getTime(); + return elapsed > 0 ? elapsed : 1; + } +} \ No newline at end of file diff --git a/src/packages/domain/entities/HostedSessionConfig.ts b/src/packages/domain/entities/HostedSessionConfig.ts new file mode 100644 index 000000000..2c27ffaa6 --- /dev/null +++ b/src/packages/domain/entities/HostedSessionConfig.ts @@ -0,0 +1,5 @@ +export interface HostedSessionConfig { + sessionName: string; + trackId: string; + carIds: string[]; +} \ No newline at end of file diff --git a/src/packages/domain/entities/StepExecution.ts b/src/packages/domain/entities/StepExecution.ts new file mode 100644 index 000000000..37634391f --- /dev/null +++ b/src/packages/domain/entities/StepExecution.ts @@ -0,0 +1,9 @@ +import { StepId } from '../value-objects/StepId'; + +export interface StepExecution { + stepId: StepId; + startedAt: Date; + completedAt?: Date; + success: boolean; + error?: string; +} \ No newline at end of file diff --git a/src/packages/domain/services/StepTransitionValidator.ts b/src/packages/domain/services/StepTransitionValidator.ts new file mode 100644 index 000000000..7e1415240 --- /dev/null +++ b/src/packages/domain/services/StepTransitionValidator.ts @@ -0,0 +1,81 @@ +import { StepId } from '../value-objects/StepId'; +import { SessionState } from '../value-objects/SessionState'; + +export interface ValidationResult { + isValid: boolean; + error?: string; +} + +const STEP_DESCRIPTIONS: Record = { + 1: 'Navigate to Hosted Racing page', + 2: 'Click Create a Race', + 3: 'Fill Race Information', + 4: 'Configure Server Details', + 5: 'Set Admins', + 6: 'Add Admin (Modal)', + 7: 'Set Time Limits', + 8: 'Set Cars', + 9: 'Add a Car (Modal)', + 10: 'Set Car Classes', + 11: 'Set Track', + 12: 'Add a Track (Modal)', + 13: 'Configure Track Options', + 14: 'Set Time of Day', + 15: 'Configure Weather', + 16: 'Set Race Options', + 17: 'Configure Team Driving', + 18: 'Track Conditions (STOP - Manual Submit Required)', +}; + +export class StepTransitionValidator { + static canTransition( + currentStep: StepId, + nextStep: StepId, + state: SessionState + ): ValidationResult { + if (!state.isInProgress()) { + return { + isValid: false, + error: 'Session must be in progress to transition steps', + }; + } + + if (currentStep.equals(nextStep)) { + return { + isValid: false, + error: 'Already at this step', + }; + } + + if (nextStep.value < currentStep.value) { + return { + isValid: false, + error: 'Cannot move backward - steps must progress forward only', + }; + } + + if (nextStep.value !== currentStep.value + 1) { + return { + isValid: false, + error: 'Cannot skip steps - must progress sequentially', + }; + } + + return { isValid: true }; + } + + static validateModalStepTransition( + currentStep: StepId, + nextStep: StepId + ): ValidationResult { + return { isValid: true }; + } + + static shouldStopAtStep18(nextStep: StepId): boolean { + return nextStep.isFinalStep(); + } + + static getStepDescription(step: StepId): string { + return STEP_DESCRIPTIONS[step.value] || `Step ${step.value}`; + } +} \ No newline at end of file diff --git a/src/packages/domain/value-objects/SessionState.ts b/src/packages/domain/value-objects/SessionState.ts new file mode 100644 index 000000000..c807e26b6 --- /dev/null +++ b/src/packages/domain/value-objects/SessionState.ts @@ -0,0 +1,81 @@ +export type SessionStateValue = + | 'PENDING' + | 'IN_PROGRESS' + | 'PAUSED' + | 'COMPLETED' + | 'FAILED' + | 'STOPPED_AT_STEP_18'; + +const VALID_STATES: SessionStateValue[] = [ + 'PENDING', + 'IN_PROGRESS', + 'PAUSED', + 'COMPLETED', + 'FAILED', + 'STOPPED_AT_STEP_18', +]; + +const VALID_TRANSITIONS: Record = { + PENDING: ['IN_PROGRESS', 'FAILED'], + IN_PROGRESS: ['PAUSED', 'COMPLETED', 'FAILED', 'STOPPED_AT_STEP_18'], + PAUSED: ['IN_PROGRESS', 'FAILED'], + COMPLETED: [], + FAILED: [], + STOPPED_AT_STEP_18: [], +}; + +export class SessionState { + private readonly _value: SessionStateValue; + + private constructor(value: SessionStateValue) { + this._value = value; + } + + static create(value: SessionStateValue): SessionState { + if (!VALID_STATES.includes(value)) { + throw new Error('Invalid session state'); + } + return new SessionState(value); + } + + get value(): SessionStateValue { + return this._value; + } + + equals(other: SessionState): boolean { + return this._value === other._value; + } + + isPending(): boolean { + return this._value === 'PENDING'; + } + + isInProgress(): boolean { + return this._value === 'IN_PROGRESS'; + } + + isCompleted(): boolean { + return this._value === 'COMPLETED'; + } + + isFailed(): boolean { + return this._value === 'FAILED'; + } + + isStoppedAtStep18(): boolean { + return this._value === 'STOPPED_AT_STEP_18'; + } + + canTransitionTo(targetState: SessionState): boolean { + const allowedTransitions = VALID_TRANSITIONS[this._value]; + return allowedTransitions.includes(targetState._value); + } + + isTerminal(): boolean { + return ( + this._value === 'COMPLETED' || + this._value === 'FAILED' || + this._value === 'STOPPED_AT_STEP_18' + ); + } +} \ No newline at end of file diff --git a/src/packages/domain/value-objects/StepId.ts b/src/packages/domain/value-objects/StepId.ts new file mode 100644 index 000000000..d59311cd6 --- /dev/null +++ b/src/packages/domain/value-objects/StepId.ts @@ -0,0 +1,40 @@ +export class StepId { + private readonly _value: number; + + private constructor(value: number) { + this._value = value; + } + + static create(value: number): StepId { + if (!Number.isInteger(value)) { + throw new Error('StepId must be an integer'); + } + if (value < 1 || value > 18) { + throw new Error('StepId must be between 1 and 18'); + } + return new StepId(value); + } + + get value(): number { + return this._value; + } + + equals(other: StepId): boolean { + return this._value === other._value; + } + + isModalStep(): boolean { + return this._value === 6 || this._value === 9 || this._value === 12; + } + + isFinalStep(): boolean { + return this._value === 18; + } + + next(): StepId { + if (this.isFinalStep()) { + throw new Error('Cannot advance beyond final step'); + } + return StepId.create(this._value + 1); + } +} \ No newline at end of file diff --git a/src/packages/shared/result/Result.ts b/src/packages/shared/result/Result.ts new file mode 100644 index 000000000..42b700a17 --- /dev/null +++ b/src/packages/shared/result/Result.ts @@ -0,0 +1,62 @@ +export class Result { + private constructor( + private readonly _value?: T, + private readonly _error?: E, + private readonly _isSuccess: boolean = true + ) {} + + static ok(value: T): Result { + return new Result(value, undefined, true); + } + + static err(error: E): Result { + return new Result(undefined, error, false); + } + + isOk(): boolean { + return this._isSuccess; + } + + isErr(): boolean { + return !this._isSuccess; + } + + unwrap(): T { + if (!this._isSuccess) { + throw new Error('Called unwrap on an error result'); + } + return this._value!; + } + + unwrapOr(defaultValue: T): T { + return this._isSuccess ? this._value! : defaultValue; + } + + unwrapErr(): E { + if (this._isSuccess) { + throw new Error('Called unwrapErr on a success result'); + } + return this._error!; + } + + map(fn: (value: T) => U): Result { + if (this._isSuccess) { + return Result.ok(fn(this._value!)); + } + return Result.err(this._error!); + } + + mapErr(fn: (error: E) => F): Result { + if (!this._isSuccess) { + return Result.err(fn(this._error!)); + } + return Result.ok(this._value!); + } + + andThen(fn: (value: T) => Result): Result { + if (this._isSuccess) { + return fn(this._value!); + } + return Result.err(this._error!); + } +} \ No newline at end of file diff --git a/tests/e2e/features/hosted-session-automation.feature b/tests/e2e/features/hosted-session-automation.feature new file mode 100644 index 000000000..66c6d5d21 --- /dev/null +++ b/tests/e2e/features/hosted-session-automation.feature @@ -0,0 +1,173 @@ +Feature: Hosted Session Automation + As a league organizer using the GridPilot companion app + I want to automate the iRacing hosted session creation workflow + So that I can quickly set up race sessions without manual data entry + + Background: + Given the companion app is running + And I am authenticated with iRacing + And I have a valid session configuration + + Scenario: Complete 18-step automation workflow + Given I have a session configuration with: + | field | value | + | sessionName | League Race Week 1 | + | trackId | spa | + | carIds | dallara-f3 | + When I start the automation session + Then the session should be created with state "PENDING" + And the current step should be 1 + + When the automation progresses through all 18 steps + Then step 1 should navigate to "Hosted Racing" + And step 2 should click "Create a Race" + And step 3 should fill "Race Information" + And step 4 should configure "Server Details" + And step 5 should access "Set Admins" + And step 6 should handle "Add an Admin" modal + And step 7 should set "Time Limits" + And step 8 should access "Set Cars" + And step 9 should handle "Add a Car" modal + And step 10 should configure "Set Car Classes" + And step 11 should access "Set Track" + And step 12 should handle "Add a Track" modal + And step 13 should configure "Track Options" + And step 14 should set "Time of Day" + And step 15 should configure "Weather" + And step 16 should set "Race Options" + And step 17 should configure "Team Driving" + And step 18 should reach "Track Conditions" + + And the session should stop at step 18 + And the session state should be "STOPPED_AT_STEP_18" + And a manual submit warning should be displayed + + Scenario: Modal step handling (step 6 - Add Admin) + Given I have started an automation session + And the automation has reached step 6 + When the "Add an Admin" modal appears + Then the automation should detect the modal + And the automation should wait for modal content to load + And the automation should fill admin fields + And the automation should close the modal + And the automation should transition to step 7 + + Scenario: Modal step handling (step 9 - Add Car) + Given I have started an automation session + And the automation has reached step 9 + When the "Add a Car" modal appears + Then the automation should detect the modal + And the automation should select the car "dallara-f3" + And the automation should confirm the selection + And the automation should close the modal + And the automation should transition to step 10 + + Scenario: Modal step handling (step 12 - Add Track) + Given I have started an automation session + And the automation has reached step 12 + When the "Add a Track" modal appears + Then the automation should detect the modal + And the automation should select the track "spa" + And the automation should confirm the selection + And the automation should close the modal + And the automation should transition to step 13 + + Scenario: Safety checkpoint at step 18 + Given I have started an automation session + And the automation has progressed to step 17 + When the automation transitions to step 18 + Then the automation should automatically stop + And the session state should be "STOPPED_AT_STEP_18" + And the current step should be 18 + And no submit action should be executed + And a notification should inform the user to review before submitting + + Scenario: Pause and resume automation + Given I have started an automation session + And the automation is at step 5 + When I pause the automation + Then the session state should be "PAUSED" + And the current step should remain 5 + + When I resume the automation + Then the session state should be "IN_PROGRESS" + And the automation should continue from step 5 + + Scenario: Automation failure handling + Given I have started an automation session + And the automation is at step 8 + When a browser automation error occurs + Then the session should transition to "FAILED" state + And an error message should be recorded + And the session should have a completedAt timestamp + And the user should be notified of the failure + + Scenario: Invalid configuration rejection + Given I have a session configuration with: + | field | value | + | sessionName | | + | trackId | spa | + | carIds | dallara-f3| + When I attempt to start the automation session + Then the session creation should fail + And an error message should indicate "Session name cannot be empty" + And no session should be persisted + + Scenario: Sequential step progression enforcement + Given I have started an automation session + And the automation is at step 5 + When I attempt to skip directly to step 7 + Then the transition should be rejected + And an error message should indicate "Cannot skip steps" + And the current step should remain 5 + + Scenario: Backward step prevention + Given I have started an automation session + And the automation has reached step 10 + When I attempt to move back to step 9 + Then the transition should be rejected + And an error message should indicate "Cannot move backward" + And the current step should remain 10 + + Scenario: Multiple car selection + Given I have a session configuration with: + | field | value | + | sessionName | Multi-class Race | + | trackId | spa | + | carIds | dallara-f3,porsche-911-gt3,bmw-m4-gt4 | + When I start the automation session + And the automation reaches step 9 + Then all three cars should be added via the modal + And the automation should handle the modal three times + And the automation should transition to step 10 + + Scenario: Session state persistence + Given I have started an automation session + And the automation has reached step 12 + When the application restarts + Then the session should be recoverable from storage + And the session state should be "IN_PROGRESS" + And the current step should be 12 + And the session configuration should be intact + + Scenario: Concurrent session prevention + Given I have started an automation session + And the session is in progress + When I attempt to start another automation session + Then the second session creation should be queued or rejected + And a warning should inform about the active session + + Scenario: Elapsed time tracking + Given I have started an automation session + When the automation runs for 5 seconds + And I query the session status + Then the elapsed time should be approximately 5000 milliseconds + And the elapsed time should increase while in progress + + Scenario: Complete workflow with realistic timings + Given I have a session configuration + When I start the automation session + Then each step should take between 200ms and 1000ms + And modal steps should take longer than regular steps + And the total workflow should complete in under 30 seconds + And the session should stop at step 18 without submitting \ No newline at end of file diff --git a/tests/e2e/step-definitions/automation.steps.ts b/tests/e2e/step-definitions/automation.steps.ts new file mode 100644 index 000000000..7de9a6154 --- /dev/null +++ b/tests/e2e/step-definitions/automation.steps.ts @@ -0,0 +1,458 @@ +import { Given, When, Then, Before, After } from '@cucumber/cucumber'; +import { expect } from 'vitest'; +import { AutomationSession } from '../../../src/packages/domain/entities/AutomationSession'; +import { StartAutomationSessionUseCase } from '../../../src/packages/application/use-cases/StartAutomationSessionUseCase'; +import { MockBrowserAutomationAdapter } from '../../../src/infrastructure/adapters/automation/MockBrowserAutomationAdapter'; +import { InMemorySessionRepository } from '../../../src/infrastructure/repositories/InMemorySessionRepository'; +import { StepId } from '../../../src/packages/domain/value-objects/StepId'; + +interface TestContext { + sessionRepository: InMemorySessionRepository; + browserAutomation: MockBrowserAutomationAdapter; + startAutomationUseCase: StartAutomationSessionUseCase; + currentSession: AutomationSession | null; + sessionConfig: any; + error: Error | null; + startTime: number; +} + +Before(function (this: TestContext) { + this.sessionRepository = new InMemorySessionRepository(); + this.browserAutomation = new MockBrowserAutomationAdapter(); + this.startAutomationUseCase = new StartAutomationSessionUseCase( + {} as any, // Mock automation engine + this.browserAutomation, + this.sessionRepository + ); + this.currentSession = null; + this.sessionConfig = {}; + this.error = null; + this.startTime = 0; +}); + +After(function (this: TestContext) { + this.currentSession = null; + this.sessionConfig = {}; + this.error = null; +}); + +Given('the companion app is running', function (this: TestContext) { + expect(this.browserAutomation).toBeDefined(); +}); + +Given('I am authenticated with iRacing', function (this: TestContext) { + // Mock authentication state + expect(true).toBe(true); +}); + +Given('I have a valid session configuration', function (this: TestContext) { + this.sessionConfig = { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }; +}); + +Given('I have a session configuration with:', function (this: TestContext, dataTable: any) { + const rows = dataTable.rawTable.slice(1); + this.sessionConfig = {}; + + rows.forEach(([field, value]: [string, string]) => { + if (field === 'carIds') { + this.sessionConfig[field] = value.split(',').map(v => v.trim()); + } else { + this.sessionConfig[field] = value; + } + }); +}); + +Given('I have started an automation session', async function (this: TestContext) { + this.sessionConfig = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + this.currentSession = AutomationSession.create(this.sessionConfig); + this.currentSession.start(); + await this.sessionRepository.save(this.currentSession); +}); + +Given('the automation has reached step {int}', async function (this: TestContext, stepNumber: number) { + expect(this.currentSession).toBeDefined(); + + for (let i = 2; i <= stepNumber; i++) { + this.currentSession!.transitionToStep(StepId.create(i)); + } + + await this.sessionRepository.update(this.currentSession!); +}); + +Given('the automation has progressed to step {int}', async function (this: TestContext, stepNumber: number) { + expect(this.currentSession).toBeDefined(); + + for (let i = 2; i <= stepNumber; i++) { + this.currentSession!.transitionToStep(StepId.create(i)); + } + + await this.sessionRepository.update(this.currentSession!); +}); + +Given('the automation is at step {int}', async function (this: TestContext, stepNumber: number) { + expect(this.currentSession).toBeDefined(); + + for (let i = 2; i <= stepNumber; i++) { + this.currentSession!.transitionToStep(StepId.create(i)); + } + + await this.sessionRepository.update(this.currentSession!); +}); + +Given('the session is in progress', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.isInProgress()).toBe(true); +}); + +When('I start the automation session', async function (this: TestContext) { + try { + const result = await this.startAutomationUseCase.execute(this.sessionConfig); + this.currentSession = await this.sessionRepository.findById(result.sessionId); + this.startTime = Date.now(); + } catch (error) { + this.error = error as Error; + } +}); + +When('I attempt to start the automation session', async function (this: TestContext) { + try { + const result = await this.startAutomationUseCase.execute(this.sessionConfig); + this.currentSession = await this.sessionRepository.findById(result.sessionId); + } catch (error) { + this.error = error as Error; + } +}); + +When('the automation progresses through all {int} steps', async function (this: TestContext, stepCount: number) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.start(); + + for (let i = 2; i <= stepCount; i++) { + this.currentSession!.transitionToStep(StepId.create(i)); + await this.browserAutomation.executeStep(StepId.create(i), this.sessionConfig); + } +}); + +When('the automation transitions to step {int}', async function (this: TestContext, stepNumber: number) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.transitionToStep(StepId.create(stepNumber)); + await this.sessionRepository.update(this.currentSession!); +}); + +When('the {string} modal appears', async function (this: TestContext, modalName: string) { + // Simulate modal appearance + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.isAtModalStep()).toBe(true); +}); + +When('I pause the automation', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.pause(); + await this.sessionRepository.update(this.currentSession!); +}); + +When('I resume the automation', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.resume(); + await this.sessionRepository.update(this.currentSession!); +}); + +When('a browser automation error occurs', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.fail('Browser automation failed at step 8'); + await this.sessionRepository.update(this.currentSession!); +}); + +When('I attempt to skip directly to step {int}', function (this: TestContext, targetStep: number) { + expect(this.currentSession).toBeDefined(); + + try { + this.currentSession!.transitionToStep(StepId.create(targetStep)); + } catch (error) { + this.error = error as Error; + } +}); + +When('I attempt to move back to step {int}', function (this: TestContext, targetStep: number) { + expect(this.currentSession).toBeDefined(); + + try { + this.currentSession!.transitionToStep(StepId.create(targetStep)); + } catch (error) { + this.error = error as Error; + } +}); + +When('the automation reaches step {int}', async function (this: TestContext, stepNumber: number) { + expect(this.currentSession).toBeDefined(); + + for (let i = 2; i <= stepNumber; i++) { + this.currentSession!.transitionToStep(StepId.create(i)); + } + + await this.sessionRepository.update(this.currentSession!); +}); + +When('the application restarts', function (this: TestContext) { + // Simulate app restart by keeping repository but clearing session reference + const sessionId = this.currentSession!.id; + this.currentSession = null; + + // Recover session + this.sessionRepository.findById(sessionId).then(session => { + this.currentSession = session; + }); +}); + +When('I attempt to start another automation session', async function (this: TestContext) { + const newConfig = { + sessionName: 'Second Race', + trackId: 'monza', + carIds: ['porsche-911-gt3'], + }; + + try { + await this.startAutomationUseCase.execute(newConfig); + } catch (error) { + this.error = error as Error; + } +}); + +When('the automation runs for {int} seconds', async function (this: TestContext, seconds: number) { + expect(this.currentSession).toBeDefined(); + // Simulate time passage + await new Promise(resolve => setTimeout(resolve, seconds * 1000)); +}); + +When('I query the session status', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + const retrieved = await this.sessionRepository.findById(this.currentSession!.id); + this.currentSession = retrieved; +}); + +Then('the session should be created with state {string}', function (this: TestContext, expectedState: string) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.value).toBe(expectedState); +}); + +Then('the current step should be {int}', function (this: TestContext, expectedStep: number) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.value).toBe(expectedStep); +}); + +Then('the current step should remain {int}', function (this: TestContext, expectedStep: number) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.value).toBe(expectedStep); +}); + +Then('step {int} should navigate to {string}', function (this: TestContext, stepNumber: number, description: string) { + // Verify step execution would happen + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should click {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should fill {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should configure {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should access {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should handle {string} modal', function (this: TestContext, stepNumber: number, modalName: string) { + expect([6, 9, 12]).toContain(stepNumber); +}); + +Then('step {int} should set {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBeGreaterThanOrEqual(1); + expect(stepNumber).toBeLessThanOrEqual(18); +}); + +Then('step {int} should reach {string}', function (this: TestContext, stepNumber: number, description: string) { + expect(stepNumber).toBe(18); +}); + +Then('the session should stop at step {int}', function (this: TestContext, expectedStep: number) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.value).toBe(expectedStep); + expect(this.currentSession!.state.isStoppedAtStep18()).toBe(true); +}); + +Then('the session state should be {string}', function (this: TestContext, expectedState: string) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.value).toBe(expectedState); +}); + +Then('a manual submit warning should be displayed', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.isFinalStep()).toBe(true); +}); + +Then('the automation should detect the modal', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.isAtModalStep()).toBe(true); +}); + +Then('the automation should wait for modal content to load', async function (this: TestContext) { + // Simulate wait + expect(this.currentSession).toBeDefined(); +}); + +Then('the automation should fill admin fields', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); +}); + +Then('the automation should close the modal', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); +}); + +Then('the automation should transition to step {int}', async function (this: TestContext, nextStep: number) { + expect(this.currentSession).toBeDefined(); + this.currentSession!.transitionToStep(StepId.create(nextStep)); +}); + +Then('the automation should select the car {string}', async function (this: TestContext, carId: string) { + expect(this.sessionConfig.carIds).toContain(carId); +}); + +Then('the automation should confirm the selection', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); +}); + +Then('the automation should select the track {string}', async function (this: TestContext, trackId: string) { + expect(this.sessionConfig.trackId).toBe(trackId); +}); + +Then('the automation should automatically stop', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.isStoppedAtStep18()).toBe(true); +}); + +Then('no submit action should be executed', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.isStoppedAtStep18()).toBe(true); +}); + +Then('a notification should inform the user to review before submitting', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.isFinalStep()).toBe(true); +}); + +Then('the automation should continue from step {int}', function (this: TestContext, expectedStep: number) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.value).toBe(expectedStep); +}); + +Then('an error message should be recorded', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.errorMessage).toBeDefined(); +}); + +Then('the session should have a completedAt timestamp', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.completedAt).toBeDefined(); +}); + +Then('the user should be notified of the failure', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.state.isFailed()).toBe(true); +}); + +Then('the session creation should fail', function (this: TestContext) { + expect(this.error).toBeDefined(); +}); + +Then('an error message should indicate {string}', function (this: TestContext, expectedMessage: string) { + expect(this.error).toBeDefined(); + expect(this.error!.message).toContain(expectedMessage); +}); + +Then('no session should be persisted', async function (this: TestContext) { + const sessions = await this.sessionRepository.findAll(); + expect(sessions).toHaveLength(0); +}); + +Then('the transition should be rejected', function (this: TestContext) { + expect(this.error).toBeDefined(); +}); + +Then('all three cars should be added via the modal', function (this: TestContext) { + expect(this.sessionConfig.carIds).toHaveLength(3); +}); + +Then('the automation should handle the modal three times', function (this: TestContext) { + expect(this.sessionConfig.carIds).toHaveLength(3); +}); + +Then('the session should be recoverable from storage', async function (this: TestContext) { + expect(this.currentSession).toBeDefined(); +}); + +Then('the session configuration should be intact', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.config).toBeDefined(); +}); + +Then('the second session creation should be queued or rejected', function (this: TestContext) { + expect(this.error).toBeDefined(); +}); + +Then('a warning should inform about the active session', function (this: TestContext) { + expect(this.error).toBeDefined(); +}); + +Then('the elapsed time should be approximately {int} milliseconds', function (this: TestContext, expectedMs: number) { + expect(this.currentSession).toBeDefined(); + const elapsed = this.currentSession!.getElapsedTime(); + expect(elapsed).toBeGreaterThanOrEqual(expectedMs - 1000); + expect(elapsed).toBeLessThanOrEqual(expectedMs + 1000); +}); + +Then('the elapsed time should increase while in progress', function (this: TestContext) { + expect(this.currentSession).toBeDefined(); + const elapsed = this.currentSession!.getElapsedTime(); + expect(elapsed).toBeGreaterThan(0); +}); + +Then('each step should take between {int}ms and {int}ms', function (this: TestContext, minMs: number, maxMs: number) { + // This would be validated during actual execution + expect(minMs).toBeLessThan(maxMs); +}); + +Then('modal steps should take longer than regular steps', function (this: TestContext) { + // This would be validated during actual execution + expect(true).toBe(true); +}); + +Then('the total workflow should complete in under {int} seconds', function (this: TestContext, maxSeconds: number) { + expect(this.currentSession).toBeDefined(); + const elapsed = this.currentSession!.getElapsedTime(); + expect(elapsed).toBeLessThan(maxSeconds * 1000); +}); + +Then('the session should stop at step {int} without submitting', function (this: TestContext, expectedStep: number) { + expect(this.currentSession).toBeDefined(); + expect(this.currentSession!.currentStep.value).toBe(expectedStep); + expect(this.currentSession!.state.isStoppedAtStep18()).toBe(true); +}); \ No newline at end of file diff --git a/tests/integration/infrastructure/InMemorySessionRepository.test.ts b/tests/integration/infrastructure/InMemorySessionRepository.test.ts new file mode 100644 index 000000000..fbc467273 --- /dev/null +++ b/tests/integration/infrastructure/InMemorySessionRepository.test.ts @@ -0,0 +1,365 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { InMemorySessionRepository } from '../../../src/infrastructure/repositories/InMemorySessionRepository'; +import { AutomationSession } from '../../../src/packages/domain/entities/AutomationSession'; +import { StepId } from '../../../src/packages/domain/value-objects/StepId'; + +describe('InMemorySessionRepository Integration Tests', () => { + let repository: InMemorySessionRepository; + + beforeEach(() => { + repository = new InMemorySessionRepository(); + }); + + describe('save', () => { + it('should persist a new session', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved).toBeDefined(); + expect(retrieved?.id).toBe(session.id); + }); + + it('should update existing session on duplicate save', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + session.start(); + session.transitionToStep(StepId.create(2)); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.currentStep.value).toBe(2); + expect(retrieved?.state.isInProgress()).toBe(true); + }); + + it('should preserve all session properties', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race Session', + trackId: 'spa-francorchamps', + carIds: ['dallara-f3', 'porsche-911-gt3'], + }); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.config.sessionName).toBe('Test Race Session'); + expect(retrieved?.config.trackId).toBe('spa-francorchamps'); + expect(retrieved?.config.carIds).toEqual(['dallara-f3', 'porsche-911-gt3']); + }); + }); + + describe('findById', () => { + it('should return null for non-existent session', async () => { + const result = await repository.findById('non-existent-id'); + + expect(result).toBeNull(); + }); + + it('should retrieve existing session by ID', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved).toBeDefined(); + expect(retrieved?.id).toBe(session.id); + }); + + it('should return domain entity not DTO', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved).toBeInstanceOf(AutomationSession); + }); + + it('should retrieve session with correct state', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + await repository.save(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.state.isInProgress()).toBe(true); + expect(retrieved?.startedAt).toBeDefined(); + }); + }); + + describe('update', () => { + it('should update existing session', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + session.start(); + session.transitionToStep(StepId.create(2)); + + await repository.update(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.currentStep.value).toBe(2); + }); + + it('should throw error when updating non-existent session', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await expect(repository.update(session)).rejects.toThrow('Session not found'); + }); + + it('should preserve unchanged properties', async () => { + const session = AutomationSession.create({ + sessionName: 'Original Name', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + session.start(); + + await repository.update(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.config.sessionName).toBe('Original Name'); + expect(retrieved?.state.isInProgress()).toBe(true); + }); + + it('should update session state correctly', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + session.start(); + session.pause(); + + await repository.update(session); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.state.value).toBe('PAUSED'); + }); + }); + + describe('delete', () => { + it('should remove session from storage', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + await repository.delete(session.id); + + const retrieved = await repository.findById(session.id); + expect(retrieved).toBeNull(); + }); + + it('should not throw when deleting non-existent session', async () => { + await expect(repository.delete('non-existent-id')).resolves.not.toThrow(); + }); + + it('should only delete specified session', async () => { + const session1 = AutomationSession.create({ + sessionName: 'Race 1', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + const session2 = AutomationSession.create({ + sessionName: 'Race 2', + trackId: 'monza', + carIds: ['porsche-911-gt3'], + }); + + await repository.save(session1); + await repository.save(session2); + + await repository.delete(session1.id); + + const retrieved1 = await repository.findById(session1.id); + const retrieved2 = await repository.findById(session2.id); + + expect(retrieved1).toBeNull(); + expect(retrieved2).toBeDefined(); + }); + }); + + describe('findAll', () => { + it('should return empty array when no sessions exist', async () => { + const sessions = await repository.findAll(); + + expect(sessions).toEqual([]); + }); + + it('should return all saved sessions', async () => { + const session1 = AutomationSession.create({ + sessionName: 'Race 1', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + const session2 = AutomationSession.create({ + sessionName: 'Race 2', + trackId: 'monza', + carIds: ['porsche-911-gt3'], + }); + + await repository.save(session1); + await repository.save(session2); + + const sessions = await repository.findAll(); + + expect(sessions).toHaveLength(2); + expect(sessions.map(s => s.id)).toContain(session1.id); + expect(sessions.map(s => s.id)).toContain(session2.id); + }); + + it('should return domain entities not DTOs', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + const sessions = await repository.findAll(); + + expect(sessions[0]).toBeInstanceOf(AutomationSession); + }); + }); + + describe('findByState', () => { + it('should return sessions matching state', async () => { + const session1 = AutomationSession.create({ + sessionName: 'Race 1', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session1.start(); + + const session2 = AutomationSession.create({ + sessionName: 'Race 2', + trackId: 'monza', + carIds: ['porsche-911-gt3'], + }); + + await repository.save(session1); + await repository.save(session2); + + const inProgressSessions = await repository.findByState('IN_PROGRESS'); + + expect(inProgressSessions).toHaveLength(1); + expect(inProgressSessions[0].id).toBe(session1.id); + }); + + it('should return empty array when no sessions match state', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + + const completedSessions = await repository.findByState('COMPLETED'); + + expect(completedSessions).toEqual([]); + }); + + it('should handle multiple sessions with same state', async () => { + const session1 = AutomationSession.create({ + sessionName: 'Race 1', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + const session2 = AutomationSession.create({ + sessionName: 'Race 2', + trackId: 'monza', + carIds: ['porsche-911-gt3'], + }); + + await repository.save(session1); + await repository.save(session2); + + const pendingSessions = await repository.findByState('PENDING'); + + expect(pendingSessions).toHaveLength(2); + }); + }); + + describe('concurrent operations', () => { + it('should handle concurrent saves', async () => { + const sessions = Array.from({ length: 10 }, (_, i) => + AutomationSession.create({ + sessionName: `Race ${i}`, + trackId: 'spa', + carIds: ['dallara-f3'], + }) + ); + + await Promise.all(sessions.map(s => repository.save(s))); + + const allSessions = await repository.findAll(); + expect(allSessions).toHaveLength(10); + }); + + it('should handle concurrent updates', async () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + await repository.save(session); + session.start(); + + await Promise.all([ + repository.update(session), + repository.update(session), + repository.update(session), + ]); + + const retrieved = await repository.findById(session.id); + expect(retrieved?.state.isInProgress()).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts b/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts new file mode 100644 index 000000000..d1771b795 --- /dev/null +++ b/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts @@ -0,0 +1,282 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { MockBrowserAutomationAdapter } from '../../../src/infrastructure/adapters/automation/MockBrowserAutomationAdapter'; +import { StepId } from '../../../src/packages/domain/value-objects/StepId'; + +describe('MockBrowserAutomationAdapter Integration Tests', () => { + let adapter: MockBrowserAutomationAdapter; + + beforeEach(() => { + adapter = new MockBrowserAutomationAdapter(); + }); + + describe('navigateToPage', () => { + it('should simulate navigation with delay', async () => { + const url = 'https://members.iracing.com/membersite/HostedRacing'; + + const result = await adapter.navigateToPage(url); + + expect(result.success).toBe(true); + expect(result.simulatedDelay).toBeGreaterThan(0); + }); + + it('should return navigation URL in result', async () => { + const url = 'https://members.iracing.com/membersite/HostedRacing'; + + const result = await adapter.navigateToPage(url); + + expect(result.url).toBe(url); + }); + + it('should simulate realistic delays', async () => { + const url = 'https://members.iracing.com/membersite/HostedRacing'; + + const result = await adapter.navigateToPage(url); + + expect(result.simulatedDelay).toBeGreaterThanOrEqual(200); + expect(result.simulatedDelay).toBeLessThanOrEqual(800); + }); + }); + + describe('fillFormField', () => { + it('should simulate form field fill with delay', async () => { + const fieldName = 'session-name'; + const value = 'Test Race Session'; + + const result = await adapter.fillFormField(fieldName, value); + + expect(result.success).toBe(true); + expect(result.fieldName).toBe(fieldName); + expect(result.value).toBe(value); + }); + + it('should simulate typing speed delay', async () => { + const fieldName = 'session-name'; + const value = 'A'.repeat(50); + + const result = await adapter.fillFormField(fieldName, value); + + expect(result.simulatedDelay).toBeGreaterThan(0); + }); + + it('should handle empty field values', async () => { + const fieldName = 'session-name'; + const value = ''; + + const result = await adapter.fillFormField(fieldName, value); + + expect(result.success).toBe(true); + expect(result.value).toBe(''); + }); + }); + + describe('clickElement', () => { + it('should simulate button click with delay', async () => { + const selector = '#create-session-button'; + + const result = await adapter.clickElement(selector); + + expect(result.success).toBe(true); + expect(result.selector).toBe(selector); + }); + + it('should simulate click delays', async () => { + const selector = '#submit-button'; + + const result = await adapter.clickElement(selector); + + expect(result.simulatedDelay).toBeGreaterThan(0); + expect(result.simulatedDelay).toBeLessThanOrEqual(300); + }); + }); + + describe('waitForElement', () => { + it('should simulate waiting for element to appear', async () => { + const selector = '.modal-dialog'; + + const result = await adapter.waitForElement(selector); + + expect(result.success).toBe(true); + expect(result.selector).toBe(selector); + }); + + it('should simulate element load time', async () => { + const selector = '.loading-spinner'; + + const result = await adapter.waitForElement(selector); + + expect(result.simulatedDelay).toBeGreaterThanOrEqual(100); + expect(result.simulatedDelay).toBeLessThanOrEqual(1000); + }); + + it('should timeout after maximum wait time', async () => { + const selector = '.non-existent-element'; + const maxWaitMs = 5000; + + const result = await adapter.waitForElement(selector, maxWaitMs); + expect(result.success).toBe(true); + }); + }); + + describe('handleModal', () => { + it('should simulate modal handling for step 6', async () => { + const stepId = StepId.create(6); + const action = 'close'; + + const result = await adapter.handleModal(stepId, action); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(6); + expect(result.action).toBe(action); + }); + + it('should simulate modal handling for step 9', async () => { + const stepId = StepId.create(9); + const action = 'confirm'; + + const result = await adapter.handleModal(stepId, action); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(9); + }); + + it('should simulate modal handling for step 12', async () => { + const stepId = StepId.create(12); + const action = 'select'; + + const result = await adapter.handleModal(stepId, action); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(12); + }); + + it('should throw error for non-modal steps', async () => { + const stepId = StepId.create(1); + const action = 'close'; + + await expect(adapter.handleModal(stepId, action)).rejects.toThrow( + 'Step 1 is not a modal step' + ); + }); + + it('should simulate modal interaction delays', async () => { + const stepId = StepId.create(6); + const action = 'close'; + + const result = await adapter.handleModal(stepId, action); + + expect(result.simulatedDelay).toBeGreaterThanOrEqual(200); + expect(result.simulatedDelay).toBeLessThanOrEqual(600); + }); + }); + + describe('executeStep', () => { + it('should execute step 1 (navigation)', async () => { + const stepId = StepId.create(1); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapter.executeStep(stepId, config); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(1); + }); + + it('should execute step 6 (modal step)', async () => { + const stepId = StepId.create(6); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapter.executeStep(stepId, config); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(6); + expect(result.wasModalStep).toBe(true); + }); + + it('should execute step 18 (final step)', async () => { + const stepId = StepId.create(18); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapter.executeStep(stepId, config); + + expect(result.success).toBe(true); + expect(result.stepId).toBe(18); + expect(result.shouldStop).toBe(true); + }); + + it('should simulate realistic step execution times', async () => { + const stepId = StepId.create(5); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapter.executeStep(stepId, config); + + expect(result.executionTime).toBeGreaterThan(0); + }); + }); + + describe('error simulation', () => { + it('should simulate random failures when enabled', async () => { + const adapterWithFailures = new MockBrowserAutomationAdapter({ + simulateFailures: true, + failureRate: 1.0, // Always fail + }); + + const stepId = StepId.create(5); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + await expect(adapterWithFailures.executeStep(stepId, config)).rejects.toThrow(); + }); + + it('should not fail when failure simulation disabled', async () => { + const adapterNoFailures = new MockBrowserAutomationAdapter({ + simulateFailures: false, + }); + + const stepId = StepId.create(5); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapterNoFailures.executeStep(stepId, config); + + expect(result.success).toBe(true); + }); + }); + + describe('performance metrics', () => { + it('should track operation metrics', async () => { + const stepId = StepId.create(1); + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const result = await adapter.executeStep(stepId, config); + + expect(result.metrics).toBeDefined(); + expect(result.metrics.totalDelay).toBeGreaterThan(0); + expect(result.metrics.operationCount).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/application/use-cases/StartAutomationSession.test.ts b/tests/unit/application/use-cases/StartAutomationSession.test.ts new file mode 100644 index 000000000..d6d227c42 --- /dev/null +++ b/tests/unit/application/use-cases/StartAutomationSession.test.ts @@ -0,0 +1,292 @@ +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { StartAutomationSessionUseCase } from '../../../../src/packages/application/use-cases/StartAutomationSessionUseCase'; +import { IAutomationEngine } from '../../../../src/packages/application/ports/IAutomationEngine'; +import { IBrowserAutomation } from '../../../../src/packages/application/ports/IBrowserAutomation'; +import { ISessionRepository } from '../../../../src/packages/application/ports/ISessionRepository'; +import { AutomationSession } from '../../../../src/packages/domain/entities/AutomationSession'; + +describe('StartAutomationSessionUseCase', () => { + let mockAutomationEngine: { + executeStep: Mock; + validateConfiguration: Mock; + }; + let mockBrowserAutomation: { + navigateToPage: Mock; + fillFormField: Mock; + clickElement: Mock; + waitForElement: Mock; + handleModal: Mock; + }; + let mockSessionRepository: { + save: Mock; + findById: Mock; + update: Mock; + delete: Mock; + }; + let useCase: StartAutomationSessionUseCase; + + beforeEach(() => { + mockAutomationEngine = { + executeStep: vi.fn(), + validateConfiguration: vi.fn(), + }; + + mockBrowserAutomation = { + navigateToPage: vi.fn(), + fillFormField: vi.fn(), + clickElement: vi.fn(), + waitForElement: vi.fn(), + handleModal: vi.fn(), + }; + + mockSessionRepository = { + save: vi.fn(), + findById: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }; + + useCase = new StartAutomationSessionUseCase( + mockAutomationEngine as unknown as IAutomationEngine, + mockBrowserAutomation as unknown as IBrowserAutomation, + mockSessionRepository as unknown as ISessionRepository + ); + }); + + describe('execute - happy path', () => { + it('should create and persist a new automation session', async () => { + const config = { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + const result = await useCase.execute(config); + + expect(result.sessionId).toBeDefined(); + expect(result.state).toBe('PENDING'); + expect(result.currentStep).toBe(1); + expect(mockAutomationEngine.validateConfiguration).toHaveBeenCalledWith(config); + expect(mockSessionRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + config, + currentStep: expect.objectContaining({ value: 1 }), + }) + ); + }); + + it('should return session DTO with correct structure', async () => { + const config = { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + const result = await useCase.execute(config); + + expect(result).toMatchObject({ + sessionId: expect.any(String), + state: 'PENDING', + currentStep: 1, + config: { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }, + }); + expect(result.startedAt).toBeUndefined(); + expect(result.completedAt).toBeUndefined(); + expect(result.errorMessage).toBeUndefined(); + }); + + it('should validate configuration before creating session', async () => { + const config = { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + await useCase.execute(config); + + expect(mockAutomationEngine.validateConfiguration).toHaveBeenCalledWith(config); + expect(mockSessionRepository.save).toHaveBeenCalled(); + }); + }); + + describe('execute - validation failures', () => { + it('should throw error for empty session name', async () => { + const config = { + sessionName: '', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + await expect(useCase.execute(config)).rejects.toThrow('Session name cannot be empty'); + expect(mockSessionRepository.save).not.toHaveBeenCalled(); + }); + + it('should throw error for missing track ID', async () => { + const config = { + sessionName: 'Test Race', + trackId: '', + carIds: ['dallara-f3'], + }; + + await expect(useCase.execute(config)).rejects.toThrow('Track ID is required'); + expect(mockSessionRepository.save).not.toHaveBeenCalled(); + }); + + it('should throw error for empty car list', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: [], + }; + + await expect(useCase.execute(config)).rejects.toThrow('At least one car must be selected'); + expect(mockSessionRepository.save).not.toHaveBeenCalled(); + }); + + it('should throw error when automation engine validation fails', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'invalid-track', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ + isValid: false, + error: 'Invalid track ID: invalid-track', + }); + + await expect(useCase.execute(config)).rejects.toThrow('Invalid track ID: invalid-track'); + expect(mockSessionRepository.save).not.toHaveBeenCalled(); + }); + + it('should throw error when automation engine validation rejects', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['invalid-car'], + }; + + mockAutomationEngine.validateConfiguration.mockRejectedValue( + new Error('Validation service unavailable') + ); + + await expect(useCase.execute(config)).rejects.toThrow('Validation service unavailable'); + expect(mockSessionRepository.save).not.toHaveBeenCalled(); + }); + }); + + describe('execute - port interactions', () => { + it('should call automation engine before saving session', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const callOrder: string[] = []; + + mockAutomationEngine.validateConfiguration.mockImplementation(async () => { + callOrder.push('validateConfiguration'); + return { isValid: true }; + }); + + mockSessionRepository.save.mockImplementation(async () => { + callOrder.push('save'); + }); + + await useCase.execute(config); + + expect(callOrder).toEqual(['validateConfiguration', 'save']); + }); + + it('should persist session with domain entity', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + await useCase.execute(config); + + expect(mockSessionRepository.save).toHaveBeenCalledWith( + expect.any(AutomationSession) + ); + }); + + it('should throw error when repository save fails', async () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockRejectedValue(new Error('Database connection failed')); + + await expect(useCase.execute(config)).rejects.toThrow('Database connection failed'); + }); + }); + + describe('execute - edge cases', () => { + it('should handle very long session names', async () => { + const config = { + sessionName: 'A'.repeat(200), + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + const result = await useCase.execute(config); + + expect(result.config.sessionName).toBe('A'.repeat(200)); + }); + + it('should handle multiple cars in configuration', async () => { + const config = { + sessionName: 'Multi-car Race', + trackId: 'spa', + carIds: ['dallara-f3', 'porsche-911-gt3', 'bmw-m4-gt4'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + const result = await useCase.execute(config); + + expect(result.config.carIds).toEqual(['dallara-f3', 'porsche-911-gt3', 'bmw-m4-gt4']); + }); + + it('should handle special characters in session name', async () => { + const config = { + sessionName: 'Test & Race #1 (2025)', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + mockAutomationEngine.validateConfiguration.mockResolvedValue({ isValid: true }); + mockSessionRepository.save.mockResolvedValue(undefined); + + const result = await useCase.execute(config); + + expect(result.config.sessionName).toBe('Test & Race #1 (2025)'); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/domain/entities/AutomationSession.test.ts b/tests/unit/domain/entities/AutomationSession.test.ts new file mode 100644 index 000000000..373bfc329 --- /dev/null +++ b/tests/unit/domain/entities/AutomationSession.test.ts @@ -0,0 +1,364 @@ +import { describe, it, expect } from 'vitest'; +import { AutomationSession } from '../../../../src/packages/domain/entities/AutomationSession'; +import { StepId } from '../../../../src/packages/domain/value-objects/StepId'; +import { SessionState } from '../../../../src/packages/domain/value-objects/SessionState'; + +describe('AutomationSession Entity', () => { + describe('create', () => { + it('should create a new session with PENDING state', () => { + const config = { + sessionName: 'Test Race Session', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + const session = AutomationSession.create(config); + + expect(session.id).toBeDefined(); + expect(session.currentStep.value).toBe(1); + expect(session.state.isPending()).toBe(true); + expect(session.config).toEqual(config); + }); + + it('should throw error for empty session name', () => { + const config = { + sessionName: '', + trackId: 'spa', + carIds: ['dallara-f3'], + }; + + expect(() => AutomationSession.create(config)).toThrow('Session name cannot be empty'); + }); + + it('should throw error for missing track ID', () => { + const config = { + sessionName: 'Test Race', + trackId: '', + carIds: ['dallara-f3'], + }; + + expect(() => AutomationSession.create(config)).toThrow('Track ID is required'); + }); + + it('should throw error for empty car list', () => { + const config = { + sessionName: 'Test Race', + trackId: 'spa', + carIds: [], + }; + + expect(() => AutomationSession.create(config)).toThrow('At least one car must be selected'); + }); + }); + + describe('start', () => { + it('should transition from PENDING to IN_PROGRESS', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + session.start(); + + expect(session.state.isInProgress()).toBe(true); + expect(session.startedAt).toBeDefined(); + }); + + it('should throw error when starting non-PENDING session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + expect(() => session.start()).toThrow('Cannot start session that is not pending'); + }); + }); + + describe('transitionToStep', () => { + it('should advance to next step when in progress', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + session.transitionToStep(StepId.create(2)); + + expect(session.currentStep.value).toBe(2); + }); + + it('should throw error when transitioning while not in progress', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + expect(() => session.transitionToStep(StepId.create(2))).toThrow( + 'Cannot transition steps when session is not in progress' + ); + }); + + it('should throw error when skipping steps', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + expect(() => session.transitionToStep(StepId.create(3))).toThrow( + 'Cannot skip steps - must transition sequentially' + ); + }); + + it('should throw error when moving backward', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + session.transitionToStep(StepId.create(2)); + + expect(() => session.transitionToStep(StepId.create(1))).toThrow( + 'Cannot move backward - steps must progress forward only' + ); + }); + + it('should stop at step 18 (safety checkpoint)', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + // Advance through all steps to 18 + for (let i = 2; i <= 18; i++) { + session.transitionToStep(StepId.create(i)); + } + + expect(session.currentStep.value).toBe(18); + expect(session.state.isStoppedAtStep18()).toBe(true); + expect(session.completedAt).toBeDefined(); + }); + }); + + describe('pause', () => { + it('should transition from IN_PROGRESS to PAUSED', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + session.pause(); + + expect(session.state.value).toBe('PAUSED'); + }); + + it('should throw error when pausing non-in-progress session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + expect(() => session.pause()).toThrow('Cannot pause session that is not in progress'); + }); + }); + + describe('resume', () => { + it('should transition from PAUSED to IN_PROGRESS', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + session.pause(); + + session.resume(); + + expect(session.state.isInProgress()).toBe(true); + }); + + it('should throw error when resuming non-paused session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + expect(() => session.resume()).toThrow('Cannot resume session that is not paused'); + }); + }); + + describe('fail', () => { + it('should transition to FAILED state with error message', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + const errorMessage = 'Browser automation failed at step 5'; + session.fail(errorMessage); + + expect(session.state.isFailed()).toBe(true); + expect(session.errorMessage).toBe(errorMessage); + expect(session.completedAt).toBeDefined(); + }); + + it('should allow failing from PENDING state', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + session.fail('Initialization failed'); + + expect(session.state.isFailed()).toBe(true); + }); + + it('should allow failing from PAUSED state', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + session.pause(); + + session.fail('Failed during pause'); + + expect(session.state.isFailed()).toBe(true); + }); + + it('should throw error when failing already completed session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + // Advance to step 18 + for (let i = 2; i <= 18; i++) { + session.transitionToStep(StepId.create(i)); + } + + expect(() => session.fail('Too late')).toThrow('Cannot fail terminal session'); + }); + }); + + describe('isAtModalStep', () => { + it('should return true when at step 6', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + for (let i = 2; i <= 6; i++) { + session.transitionToStep(StepId.create(i)); + } + + expect(session.isAtModalStep()).toBe(true); + }); + + it('should return true when at step 9', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + for (let i = 2; i <= 9; i++) { + session.transitionToStep(StepId.create(i)); + } + + expect(session.isAtModalStep()).toBe(true); + }); + + it('should return true when at step 12', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + for (let i = 2; i <= 12; i++) { + session.transitionToStep(StepId.create(i)); + } + + expect(session.isAtModalStep()).toBe(true); + }); + + it('should return false when at non-modal step', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + expect(session.isAtModalStep()).toBe(false); + }); + }); + + describe('getElapsedTime', () => { + it('should return 0 for non-started session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + + expect(session.getElapsedTime()).toBe(0); + }); + + it('should return elapsed milliseconds for in-progress session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + // Wait a bit (in real test, this would be mocked) + const elapsed = session.getElapsedTime(); + expect(elapsed).toBeGreaterThan(0); + }); + + it('should return total duration for completed session', () => { + const session = AutomationSession.create({ + sessionName: 'Test Race', + trackId: 'spa', + carIds: ['dallara-f3'], + }); + session.start(); + + // Advance to step 18 + for (let i = 2; i <= 18; i++) { + session.transitionToStep(StepId.create(i)); + } + + const elapsed = session.getElapsedTime(); + expect(elapsed).toBeGreaterThan(0); + expect(session.state.isStoppedAtStep18()).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/domain/services/StepTransitionValidator.test.ts b/tests/unit/domain/services/StepTransitionValidator.test.ts new file mode 100644 index 000000000..4e3e7a8f8 --- /dev/null +++ b/tests/unit/domain/services/StepTransitionValidator.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect } from 'vitest'; +import { StepTransitionValidator } from '../../../../src/packages/domain/services/StepTransitionValidator'; +import { StepId } from '../../../../src/packages/domain/value-objects/StepId'; +import { SessionState } from '../../../../src/packages/domain/value-objects/SessionState'; + +describe('StepTransitionValidator Service', () => { + describe('canTransition', () => { + it('should allow sequential forward transition', () => { + const currentStep = StepId.create(1); + const nextStep = StepId.create(2); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should reject transition when not IN_PROGRESS', () => { + const currentStep = StepId.create(1); + const nextStep = StepId.create(2); + const state = SessionState.create('PENDING'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Session must be in progress to transition steps'); + }); + + it('should reject skipping steps', () => { + const currentStep = StepId.create(1); + const nextStep = StepId.create(3); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Cannot skip steps - must progress sequentially'); + }); + + it('should reject backward transitions', () => { + const currentStep = StepId.create(5); + const nextStep = StepId.create(4); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Cannot move backward - steps must progress forward only'); + }); + + it('should reject same step transition', () => { + const currentStep = StepId.create(5); + const nextStep = StepId.create(5); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Already at this step'); + }); + + it('should allow transition through modal steps', () => { + const currentStep = StepId.create(5); + const nextStep = StepId.create(6); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(true); + }); + + it('should allow transition from modal step to next', () => { + const currentStep = StepId.create(6); + const nextStep = StepId.create(7); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(true); + }); + }); + + describe('validateModalStepTransition', () => { + it('should allow entering modal step 6', () => { + const currentStep = StepId.create(5); + const nextStep = StepId.create(6); + + const result = StepTransitionValidator.validateModalStepTransition(currentStep, nextStep); + + expect(result.isValid).toBe(true); + }); + + it('should allow entering modal step 9', () => { + const currentStep = StepId.create(8); + const nextStep = StepId.create(9); + + const result = StepTransitionValidator.validateModalStepTransition(currentStep, nextStep); + + expect(result.isValid).toBe(true); + }); + + it('should allow entering modal step 12', () => { + const currentStep = StepId.create(11); + const nextStep = StepId.create(12); + + const result = StepTransitionValidator.validateModalStepTransition(currentStep, nextStep); + + expect(result.isValid).toBe(true); + }); + + it('should allow exiting modal step 6', () => { + const currentStep = StepId.create(6); + const nextStep = StepId.create(7); + + const result = StepTransitionValidator.validateModalStepTransition(currentStep, nextStep); + + expect(result.isValid).toBe(true); + }); + + it('should allow non-modal transitions', () => { + const currentStep = StepId.create(1); + const nextStep = StepId.create(2); + + const result = StepTransitionValidator.validateModalStepTransition(currentStep, nextStep); + + expect(result.isValid).toBe(true); + }); + }); + + describe('shouldStopAtStep18', () => { + it('should return true when transitioning to step 18', () => { + const nextStep = StepId.create(18); + + const shouldStop = StepTransitionValidator.shouldStopAtStep18(nextStep); + + expect(shouldStop).toBe(true); + }); + + it('should return false for steps before 18', () => { + const nextStep = StepId.create(17); + + const shouldStop = StepTransitionValidator.shouldStopAtStep18(nextStep); + + expect(shouldStop).toBe(false); + }); + + it('should return false for early steps', () => { + const nextStep = StepId.create(1); + + const shouldStop = StepTransitionValidator.shouldStopAtStep18(nextStep); + + expect(shouldStop).toBe(false); + }); + }); + + describe('getStepDescription', () => { + it('should return description for step 1', () => { + const step = StepId.create(1); + + const description = StepTransitionValidator.getStepDescription(step); + + expect(description).toBe('Navigate to Hosted Racing page'); + }); + + it('should return description for step 6 (modal)', () => { + const step = StepId.create(6); + + const description = StepTransitionValidator.getStepDescription(step); + + expect(description).toBe('Add Admin (Modal)'); + }); + + it('should return description for step 18 (final)', () => { + const step = StepId.create(18); + + const description = StepTransitionValidator.getStepDescription(step); + + expect(description).toBe('Track Conditions (STOP - Manual Submit Required)'); + }); + + it('should return descriptions for all modal steps', () => { + const modalSteps = [6, 9, 12]; + + modalSteps.forEach(stepNum => { + const step = StepId.create(stepNum); + const description = StepTransitionValidator.getStepDescription(step); + expect(description).toContain('(Modal)'); + }); + }); + }); + + describe('edge cases', () => { + it('should handle rapid sequential transitions', () => { + const state = SessionState.create('IN_PROGRESS'); + let currentStep = StepId.create(1); + + for (let i = 2; i <= 18; i++) { + const nextStep = StepId.create(i); + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(true); + currentStep = nextStep; + } + }); + + it('should prevent transitions from terminal states', () => { + const terminalStates = ['COMPLETED', 'FAILED', 'STOPPED_AT_STEP_18'] as const; + + terminalStates.forEach(stateValue => { + const currentStep = StepId.create(10); + const nextStep = StepId.create(11); + const state = SessionState.create(stateValue); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(false); + }); + }); + + it('should allow transition from PAUSED when resumed', () => { + const currentStep = StepId.create(5); + const nextStep = StepId.create(6); + const state = SessionState.create('IN_PROGRESS'); + + const result = StepTransitionValidator.canTransition(currentStep, nextStep, state); + + expect(result.isValid).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/domain/value-objects/SessionState.test.ts b/tests/unit/domain/value-objects/SessionState.test.ts new file mode 100644 index 000000000..a762481c2 --- /dev/null +++ b/tests/unit/domain/value-objects/SessionState.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from 'vitest'; +import { SessionState } from '../../../../src/packages/domain/value-objects/SessionState'; + +describe('SessionState Value Object', () => { + describe('create', () => { + it('should create PENDING state', () => { + const state = SessionState.create('PENDING'); + expect(state.value).toBe('PENDING'); + }); + + it('should create IN_PROGRESS state', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.value).toBe('IN_PROGRESS'); + }); + + it('should create PAUSED state', () => { + const state = SessionState.create('PAUSED'); + expect(state.value).toBe('PAUSED'); + }); + + it('should create COMPLETED state', () => { + const state = SessionState.create('COMPLETED'); + expect(state.value).toBe('COMPLETED'); + }); + + it('should create FAILED state', () => { + const state = SessionState.create('FAILED'); + expect(state.value).toBe('FAILED'); + }); + + it('should create STOPPED_AT_STEP_18 state', () => { + const state = SessionState.create('STOPPED_AT_STEP_18'); + expect(state.value).toBe('STOPPED_AT_STEP_18'); + }); + + it('should throw error for invalid state', () => { + expect(() => SessionState.create('INVALID' as any)).toThrow('Invalid session state'); + }); + + it('should throw error for empty string', () => { + expect(() => SessionState.create('' as any)).toThrow('Invalid session state'); + }); + }); + + describe('equals', () => { + it('should return true for equal states', () => { + const state1 = SessionState.create('PENDING'); + const state2 = SessionState.create('PENDING'); + expect(state1.equals(state2)).toBe(true); + }); + + it('should return false for different states', () => { + const state1 = SessionState.create('PENDING'); + const state2 = SessionState.create('IN_PROGRESS'); + expect(state1.equals(state2)).toBe(false); + }); + }); + + describe('isPending', () => { + it('should return true for PENDING state', () => { + const state = SessionState.create('PENDING'); + expect(state.isPending()).toBe(true); + }); + + it('should return false for IN_PROGRESS state', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.isPending()).toBe(false); + }); + }); + + describe('isInProgress', () => { + it('should return true for IN_PROGRESS state', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.isInProgress()).toBe(true); + }); + + it('should return false for PENDING state', () => { + const state = SessionState.create('PENDING'); + expect(state.isInProgress()).toBe(false); + }); + }); + + describe('isCompleted', () => { + it('should return true for COMPLETED state', () => { + const state = SessionState.create('COMPLETED'); + expect(state.isCompleted()).toBe(true); + }); + + it('should return false for IN_PROGRESS state', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.isCompleted()).toBe(false); + }); + }); + + describe('isFailed', () => { + it('should return true for FAILED state', () => { + const state = SessionState.create('FAILED'); + expect(state.isFailed()).toBe(true); + }); + + it('should return false for COMPLETED state', () => { + const state = SessionState.create('COMPLETED'); + expect(state.isFailed()).toBe(false); + }); + }); + + describe('isStoppedAtStep18', () => { + it('should return true for STOPPED_AT_STEP_18 state', () => { + const state = SessionState.create('STOPPED_AT_STEP_18'); + expect(state.isStoppedAtStep18()).toBe(true); + }); + + it('should return false for COMPLETED state', () => { + const state = SessionState.create('COMPLETED'); + expect(state.isStoppedAtStep18()).toBe(false); + }); + }); + + describe('canTransitionTo', () => { + it('should allow transition from PENDING to IN_PROGRESS', () => { + const state = SessionState.create('PENDING'); + expect(state.canTransitionTo(SessionState.create('IN_PROGRESS'))).toBe(true); + }); + + it('should allow transition from IN_PROGRESS to PAUSED', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.canTransitionTo(SessionState.create('PAUSED'))).toBe(true); + }); + + it('should allow transition from IN_PROGRESS to STOPPED_AT_STEP_18', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.canTransitionTo(SessionState.create('STOPPED_AT_STEP_18'))).toBe(true); + }); + + it('should allow transition from PAUSED to IN_PROGRESS', () => { + const state = SessionState.create('PAUSED'); + expect(state.canTransitionTo(SessionState.create('IN_PROGRESS'))).toBe(true); + }); + + it('should not allow transition from COMPLETED to IN_PROGRESS', () => { + const state = SessionState.create('COMPLETED'); + expect(state.canTransitionTo(SessionState.create('IN_PROGRESS'))).toBe(false); + }); + + it('should not allow transition from FAILED to IN_PROGRESS', () => { + const state = SessionState.create('FAILED'); + expect(state.canTransitionTo(SessionState.create('IN_PROGRESS'))).toBe(false); + }); + + it('should not allow transition from STOPPED_AT_STEP_18 to IN_PROGRESS', () => { + const state = SessionState.create('STOPPED_AT_STEP_18'); + expect(state.canTransitionTo(SessionState.create('IN_PROGRESS'))).toBe(false); + }); + }); + + describe('isTerminal', () => { + it('should return true for COMPLETED state', () => { + const state = SessionState.create('COMPLETED'); + expect(state.isTerminal()).toBe(true); + }); + + it('should return true for FAILED state', () => { + const state = SessionState.create('FAILED'); + expect(state.isTerminal()).toBe(true); + }); + + it('should return true for STOPPED_AT_STEP_18 state', () => { + const state = SessionState.create('STOPPED_AT_STEP_18'); + expect(state.isTerminal()).toBe(true); + }); + + it('should return false for PENDING state', () => { + const state = SessionState.create('PENDING'); + expect(state.isTerminal()).toBe(false); + }); + + it('should return false for IN_PROGRESS state', () => { + const state = SessionState.create('IN_PROGRESS'); + expect(state.isTerminal()).toBe(false); + }); + + it('should return false for PAUSED state', () => { + const state = SessionState.create('PAUSED'); + expect(state.isTerminal()).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/domain/value-objects/StepId.test.ts b/tests/unit/domain/value-objects/StepId.test.ts new file mode 100644 index 000000000..f62100ec8 --- /dev/null +++ b/tests/unit/domain/value-objects/StepId.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest'; +import { StepId } from '../../../../src/packages/domain/value-objects/StepId'; + +describe('StepId Value Object', () => { + describe('create', () => { + it('should create a valid StepId for step 1', () => { + const stepId = StepId.create(1); + expect(stepId.value).toBe(1); + }); + + it('should create a valid StepId for step 18', () => { + const stepId = StepId.create(18); + expect(stepId.value).toBe(18); + }); + + it('should throw error for step 0 (below minimum)', () => { + expect(() => StepId.create(0)).toThrow('StepId must be between 1 and 18'); + }); + + it('should throw error for step 19 (above maximum)', () => { + expect(() => StepId.create(19)).toThrow('StepId must be between 1 and 18'); + }); + + it('should throw error for negative step', () => { + expect(() => StepId.create(-1)).toThrow('StepId must be between 1 and 18'); + }); + + it('should throw error for non-integer step', () => { + expect(() => StepId.create(5.5)).toThrow('StepId must be an integer'); + }); + }); + + describe('equals', () => { + it('should return true for equal StepIds', () => { + const stepId1 = StepId.create(5); + const stepId2 = StepId.create(5); + expect(stepId1.equals(stepId2)).toBe(true); + }); + + it('should return false for different StepIds', () => { + const stepId1 = StepId.create(5); + const stepId2 = StepId.create(6); + expect(stepId1.equals(stepId2)).toBe(false); + }); + }); + + describe('isModalStep', () => { + it('should return true for step 6 (add admin modal)', () => { + const stepId = StepId.create(6); + expect(stepId.isModalStep()).toBe(true); + }); + + it('should return true for step 9 (add car modal)', () => { + const stepId = StepId.create(9); + expect(stepId.isModalStep()).toBe(true); + }); + + it('should return true for step 12 (add track modal)', () => { + const stepId = StepId.create(12); + expect(stepId.isModalStep()).toBe(true); + }); + + it('should return false for non-modal step', () => { + const stepId = StepId.create(1); + expect(stepId.isModalStep()).toBe(false); + }); + }); + + describe('isFinalStep', () => { + it('should return true for step 18', () => { + const stepId = StepId.create(18); + expect(stepId.isFinalStep()).toBe(true); + }); + + it('should return false for step 17', () => { + const stepId = StepId.create(17); + expect(stepId.isFinalStep()).toBe(false); + }); + + it('should return false for step 1', () => { + const stepId = StepId.create(1); + expect(stepId.isFinalStep()).toBe(false); + }); + }); + + describe('next', () => { + it('should return next step for step 1', () => { + const stepId = StepId.create(1); + const nextStep = stepId.next(); + expect(nextStep.value).toBe(2); + }); + + it('should return next step for step 17', () => { + const stepId = StepId.create(17); + const nextStep = stepId.next(); + expect(nextStep.value).toBe(18); + }); + + it('should throw error when calling next on step 18', () => { + const stepId = StepId.create(18); + expect(() => stepId.next()).toThrow('Cannot advance beyond final step'); + }); + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..19c61ad48 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "types": ["vitest/globals"] + }, + "include": [ + "src/**/*", + "tests/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.js" + ] +} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..f3e2180b7 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + exclude: ['tests/e2e/**/*'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'tests/', + '**/*.d.ts', + '**/*.config.*', + '**/dist/**', + ], + }, + }, +}); \ No newline at end of file