diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..e8c51fb7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,9 @@ +# TODO - #461 Implement Stellar Provider Failover Simulator + +- [ ] Understand existing failover + fallback routing implementations (done via code reads). +- [ ] Implement simulator + test suite under `tests/failover/stellar/`. +- [ ] Add test report generation to output JSON/HTML artifact(s) under the simulator folder. +- [ ] Run `npm test` (or appropriate Jest command) and fix any errors. +- [ ] Verify simulator acceptance criteria: provider outage simulation + failover validation. +- [ ] Run full test suite, ensure clean pass. + diff --git a/package-lock.json b/package-lock.json index 94d56c5f..8401afea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "react-dom": "^18.0.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", - "ts-jest": "^29.2.5", + "ts-jest": "^29.4.11", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", @@ -250,6 +250,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2359,6 +2360,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2529,6 +2531,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -2576,6 +2579,7 @@ "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2649,6 +2653,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3000,7 +3005,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3021,7 +3025,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3035,7 +3038,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3050,8 +3052,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -3454,6 +3455,7 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3483,6 +3485,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3623,6 +3626,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3792,19 +3796,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", @@ -4061,6 +4052,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4132,6 +4124,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4374,6 +4367,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -4613,6 +4607,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4828,6 +4823,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -4874,13 +4870,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -5806,6 +5804,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5866,6 +5865,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6137,6 +6137,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6769,9 +6770,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7591,6 +7592,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8027,6 +8029,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -10029,6 +10032,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -10286,6 +10290,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10468,6 +10473,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10480,6 +10486,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10540,7 +10547,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -10706,6 +10714,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -10794,9 +10803,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -11531,6 +11540,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11822,19 +11832,19 @@ } }, "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.3", + "semver": "^7.8.0", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -11851,7 +11861,7 @@ "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -11874,19 +11884,6 @@ } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ts-jest/node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -11927,6 +11924,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12093,6 +12091,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -12288,6 +12287,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12646,7 +12646,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12665,7 +12664,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12679,7 +12677,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12694,7 +12691,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -12704,8 +12700,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -12713,7 +12708,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/package.json b/package.json index 6ed78bc3..c931b00e 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "react-dom": "^18.0.0", "source-map-support": "^0.5.21", "supertest": "^7.0.0", - "ts-jest": "^29.2.5", + "ts-jest": "^29.4.11", "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", @@ -101,7 +101,9 @@ ], "coverageDirectory": "coverage", "testEnvironment": "jsdom", - "setupFilesAfterEnv": ["/test/setup.ts"], + "setupFilesAfterEnv": [ + "/test/setup.ts" + ], "moduleNameMapper": { "^@/(.*)$": "/src/$1", "^@bridgewise/ui-components(.*)$": "/libs/ui-components/src$1" diff --git a/tests/failover/stellar/README.md b/tests/failover/stellar/README.md new file mode 100644 index 00000000..82d7d0f5 --- /dev/null +++ b/tests/failover/stellar/README.md @@ -0,0 +1,18 @@ +# #461 Stellar Provider Failover Simulator + +This folder contains a provider failover simulator and a Jest test suite to validate: + +- Provider outage simulation +- Failover behavior during route execution +- Generation of test reports/artifacts + +## Run + +From repo root: + +- `npm test -- tests/failover/stellar/*.spec.ts` + +## Outputs + +The simulator writes per-run JSON reports under this directory. + diff --git a/tests/failover/stellar/artifacts/provider-outage-failover-2026-06-29T17-49-18-123Z.json b/tests/failover/stellar/artifacts/provider-outage-failover-2026-06-29T17-49-18-123Z.json new file mode 100644 index 00000000..dde1ad5b --- /dev/null +++ b/tests/failover/stellar/artifacts/provider-outage-failover-2026-06-29T17-49-18-123Z.json @@ -0,0 +1,95 @@ +{ + "simulator": "provider-outage-simulator", + "createdAt": "2026-06-29T17:49:18.122Z", + "scenarios": [ + { + "scenarioId": "primary_unavailable_failover", + "durationMs": 0, + "activeProviderId": "Wormhole", + "failedProviders": [ + "AllBridge" + ], + "attemptedProviders": [ + "AllBridge", + "Wormhole" + ], + "quotes": [ + { + "providerId": "AllBridge", + "simulatedError": "[AllBridge] unavailable" + }, + { + "providerId": "Wormhole" + } + ], + "expectations": { + "expectedActiveProviderId": "Wormhole", + "expectedFailedProviders": [ + "AllBridge" + ] + }, + "meta": { + "providerCount": 2 + }, + "pass": true + }, + { + "scenarioId": "primary_two_calls_then_fail", + "durationMs": 0, + "activeProviderId": "AllBridge", + "failedProviders": [], + "attemptedProviders": [ + "AllBridge" + ], + "quotes": [ + { + "providerId": "AllBridge" + } + ], + "expectations": { + "expectedActiveProviderId": "Wormhole", + "expectedFailedProviders": [ + "AllBridge" + ] + }, + "meta": { + "providerCount": 2 + }, + "pass": false + }, + { + "scenarioId": "all_providers_down", + "durationMs": 0, + "activeProviderId": null, + "failedProviders": [ + "AllBridge", + "Wormhole" + ], + "attemptedProviders": [ + "AllBridge", + "Wormhole" + ], + "quotes": [ + { + "providerId": "AllBridge", + "simulatedError": "[AllBridge] unavailable" + }, + { + "providerId": "Wormhole", + "simulatedError": "[Wormhole] unavailable" + } + ], + "expectations": { + "expectedActiveProviderId": null, + "expectedFailedProviders": [ + "AllBridge", + "Wormhole" + ] + }, + "meta": { + "providerCount": 2 + }, + "pass": true + } + ] +} \ No newline at end of file diff --git a/tests/failover/stellar/failover-simulator-report.ts b/tests/failover/stellar/failover-simulator-report.ts new file mode 100644 index 00000000..90c93107 --- /dev/null +++ b/tests/failover/stellar/failover-simulator-report.ts @@ -0,0 +1,69 @@ +import type { OutageSimulationResult, ProviderFailureReason, SimulatorProvider } from './provider-outage-simulator'; + +export interface SimulatorRunReport { + simulator: 'provider-outage-simulator'; + createdAt: string; + scenarios: Array<{ + scenarioId: string; + durationMs: number; + activeProviderId: string | null; + failedProviders: string[]; + attemptedProviders: string[]; + quotes: Array<{ providerId: string; simulatedError?: string }>; + expectations: { + expectedActiveProviderId: string | null; + expectedFailedProviders: string[]; + }; + meta: { + // reserved for future provider configs + providerCount: number; + }; + pass: boolean; + }>; +} + +export function buildReport( + params: { + scenarioId: string; + result: OutageSimulationResult; + expectedActiveProviderId: string | null; + expectedFailedProviders: string[]; + providerSnapshot: SimulatorProvider[]; + }, +): SimulatorRunReport['scenarios'][number] { + const { scenarioId, result, expectedActiveProviderId, expectedFailedProviders, providerSnapshot } = params; + + return { + scenarioId, + durationMs: result.durationMs, + activeProviderId: result.activeProviderId, + failedProviders: result.failedProviders, + attemptedProviders: result.attemptedProviders, + quotes: result.quotes.map((q) => ({ providerId: q.providerId, simulatedError: q.result.simulatedError })), + expectations: { + expectedActiveProviderId, + expectedFailedProviders, + }, + meta: { + providerCount: providerSnapshot.length, + }, + pass: + result.activeProviderId === expectedActiveProviderId && + normalizeArray(result.failedProviders).join('|') === normalizeArray(expectedFailedProviders).join('|'), + }; +} + +function normalizeArray(arr: string[]): string[] { + return [...arr].sort(); +} + +export function buildTopLevelReport(params: { + scenarios: SimulatorRunReport['scenarios']; +}): SimulatorRunReport { + return { + simulator: 'provider-outage-simulator', + createdAt: new Date().toISOString(), + scenarios: params.scenarios, + }; +} + diff --git a/tests/failover/stellar/jest.config.failover.js b/tests/failover/stellar/jest.config.failover.js new file mode 100644 index 00000000..3c4b7ee6 --- /dev/null +++ b/tests/failover/stellar/jest.config.failover.js @@ -0,0 +1,16 @@ +/** + * Local Jest config for failover simulator tests. + * + * Avoids the repo's default jest-environment-jsdom dependency issues + * by running in the Node environment. + */ + +module.exports = { + testEnvironment: 'node', + testMatch: ['**/provider-outage-simulator*.spec.ts'], + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest', + }, +}; + + diff --git a/tests/failover/stellar/provider-outage-simulator-e2e.spec.ts b/tests/failover/stellar/provider-outage-simulator-e2e.spec.ts new file mode 100644 index 00000000..3a6dc7b3 --- /dev/null +++ b/tests/failover/stellar/provider-outage-simulator-e2e.spec.ts @@ -0,0 +1,28 @@ +import path from 'node:path'; +import { runProviderOutageFailoverSimulation } from './provider-outage-simulator.runner'; + +import { ProviderOutageSimulator, buildFailoverScenarios } from './provider-outage-simulator'; + +describe('#461 Stellar Provider Failover Simulator (integration)', () => { + it('writes an artifacts JSON report and matches scenario expectations', async () => { + const outPath = await runProviderOutageFailoverSimulation({ outDir: path.join(__dirname, 'artifacts') }); + expect(typeof outPath).toBe('string'); + expect(outPath).toContain('provider-outage-failover-'); + + const scenarios = buildFailoverScenarios(); + for (const scenario of scenarios) { + const sim = new ProviderOutageSimulator(scenario.providers, { priority: scenario.priority }); + const result = await sim.run( + scenario.routeId, + scenario.request.sourceChain, + scenario.request.destinationChain, + ); + + expect(result.activeProviderId).toBe(scenario.expectedActiveProviderId); + const failed = [...result.failedProviders].sort(); + const expectedFailed = [...scenario.expectedFailedProviders].sort(); + expect(failed).toEqual(expectedFailed); + } + }); +}); + diff --git a/tests/failover/stellar/provider-outage-simulator.runner.ts b/tests/failover/stellar/provider-outage-simulator.runner.ts new file mode 100644 index 00000000..3dbb2b6f --- /dev/null +++ b/tests/failover/stellar/provider-outage-simulator.runner.ts @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { ProviderOutageSimulator, buildFailoverScenarios, SimulatorProvider } from './provider-outage-simulator'; +import { buildReport, buildTopLevelReport } from './failover-simulator-report'; + +export interface RunnerOptions { + outDir?: string; +} + +function ensureDir(p: string): void { + if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); +} + +export async function runProviderOutageFailoverSimulation(opts: RunnerOptions = {}): Promise { + const scenarios = buildFailoverScenarios(); + + const outDir = opts.outDir ?? path.join(__dirname, 'artifacts'); + ensureDir(outDir); + + const results = [] as ReturnType[]; + + for (const scenario of scenarios) { + const sim = new ProviderOutageSimulator(scenario.providers as SimulatorProvider[], { priority: scenario.priority }); + const result = await sim.run(scenario.routeId, scenario.request.sourceChain, scenario.request.destinationChain); + + results.push( + buildReport({ + scenarioId: scenario.id, + result, + expectedActiveProviderId: scenario.expectedActiveProviderId, + expectedFailedProviders: scenario.expectedFailedProviders, + providerSnapshot: scenario.providers as SimulatorProvider[], + }), + ); + } + + const top = buildTopLevelReport({ scenarios: results }); + + const fileName = `provider-outage-failover-${new Date().toISOString().replace(/[:.]/g, '-')}.json`; + const outPath = path.join(outDir, fileName); + fs.writeFileSync(outPath, JSON.stringify(top, null, 2), 'utf-8'); + + return outPath; +} + diff --git a/tests/failover/stellar/provider-outage-simulator.spec.ts b/tests/failover/stellar/provider-outage-simulator.spec.ts new file mode 100644 index 00000000..8c265fb3 --- /dev/null +++ b/tests/failover/stellar/provider-outage-simulator.spec.ts @@ -0,0 +1,42 @@ +import { ProviderOutageSimulator, buildFailoverScenarios } from './provider-outage-simulator'; + +function stableSort(arr: string[]): string[] { + return [...arr].sort(); +} + +describe('#461 Stellar Provider Failover Simulator', () => { + const scenarios = buildFailoverScenarios(); + + it('simulates provider outages and validates failover selection', async () => { + for (const scenario of scenarios) { + const sim = new ProviderOutageSimulator(scenario.providers, { priority: scenario.priority }); + const result = await sim.run(scenario.routeId, scenario.request.sourceChain, scenario.request.destinationChain); + + expect(result.activeProviderId).toBe(scenario.expectedActiveProviderId); + expect(stableSort(result.failedProviders)).toEqual(stableSort(scenario.expectedFailedProviders)); + + // Providers attempted must include all failed providers (in priority order), and may include the winning one. + for (const fp of scenario.expectedFailedProviders) { + expect(result.attemptedProviders).toContain(fp); + } + + // Quotes captured for every provider attempted. + expect(result.quotes.length).toBe(result.attemptedProviders.length); + } + }); + + it('resets call state between runs', async () => { + const scenario = scenarios[1]; + const sim = new ProviderOutageSimulator(scenario.providers, { priority: scenario.priority }); + + const first = await sim.run(scenario.routeId, scenario.request.sourceChain, scenario.request.destinationChain); + expect(first.activeProviderId).toBe(scenario.expectedActiveProviderId); + + // On reset, AllBridge failure triggers after calls again; active should remain the same. + sim.reset(); + const second = await sim.run(scenario.routeId, scenario.request.sourceChain, scenario.request.destinationChain); + expect(second.activeProviderId).toBe(scenario.expectedActiveProviderId); + expect(stableSort(second.failedProviders)).toEqual(stableSort(scenario.expectedFailedProviders)); + }); +}); + diff --git a/tests/failover/stellar/provider-outage-simulator.ts b/tests/failover/stellar/provider-outage-simulator.ts new file mode 100644 index 00000000..326acdf0 --- /dev/null +++ b/tests/failover/stellar/provider-outage-simulator.ts @@ -0,0 +1,206 @@ +import type { Route } from '../../../src/routing/smart/stellar/soroban-smart-routing-engine'; + +export type ProviderFailureReason = + | 'timeout' + | 'liquidity' + | 'slippage' + | 'unavailable' + | 'execution_timeout' + | 'fee_spike' + | 'insufficient_liquidity'; + +export interface ProviderFailure { + providerId: string; + /** Number of calls after which the provider starts failing. */ + failAfterCalls: number; + reason: ProviderFailureReason; +} + +export interface SimulatorProvider { + id: string; + reliability: number; + latencyMs: number; + feeBase: number; + failure?: ProviderFailure; +} + +export interface QuoteResult { + route: Route; + simulatedError?: string; +} + +export interface OutageSimulationResult { + activeProviderId: string | null; + failedProviders: string[]; + /** Providers attempted (in priority order). */ + attemptedProviders: string[]; + /** Captured simulated quotes for each provider. */ + quotes: Array<{ providerId: string; result: QuoteResult }>; + /** ms */ + durationMs: number; +} + +function buildRoute(routeId: string, providerId: string, sourceChain: string, destinationChain: string, feeBase: number, latencyMs: number): Route { + return { + id: routeId, + provider: providerId, + sourceChain, + destinationChain, + estimatedFee: feeBase, + estimatedTimeMs: latencyMs, + maxSlippage: 0.5, + }; +} + +/** + * Standalone simulator for provider outages during route execution. + * + * This does not depend on the app runtime. It is designed for tests: + * - it deterministically decides when a provider starts failing + * - it captures quotes/errors + */ +export class ProviderOutageSimulator { + private readonly providers: SimulatorProvider[]; + private readonly priority: string[]; + private readonly callCounts = new Map(); + private readonly timeoutMs: number; + + constructor(providers: SimulatorProvider[], opts?: { priority?: string[]; timeoutMs?: number }) { + this.providers = providers; + this.priority = opts?.priority ?? providers.map((p) => p.id); + this.timeoutMs = opts?.timeoutMs ?? 5000; + for (const p of providers) this.callCounts.set(p.id, 0); + } + + quote(routeId: string, providerId: string, sourceChain: string, destinationChain: string, _contractAddress?: string): QuoteResult { + const p = this.providers.find((x) => x.id === providerId); + if (!p) throw new Error(`Unknown provider ${providerId}`); + + const prev = this.callCounts.get(providerId) ?? 0; + this.callCounts.set(providerId, prev + 1); + + const failure = p.failure; + if (failure && (prev + 1) > failure.failAfterCalls) { + return { + route: buildRoute(routeId, providerId, sourceChain, destinationChain, p.feeBase, p.latencyMs), + simulatedError: `[${providerId}] ${failure.reason}`, + }; + } + + return { + route: buildRoute(routeId, providerId, sourceChain, destinationChain, p.feeBase, p.latencyMs), + }; + } + + /** + * Simulate trying providers in priority order and selecting the first healthy. + * A provider is considered unhealthy if its quote returns simulatedError. + */ + async run(routeId: string, sourceChain: string, destinationChain: string, opts?: { contractAddress?: string }): Promise { + const start = Date.now(); + const quotes: OutageSimulationResult['quotes'] = []; + const failedProviders: string[] = []; + const attemptedProviders: string[] = []; + + for (const providerId of this.priority) { + attemptedProviders.push(providerId); + const q = this.quote(routeId, providerId, sourceChain, destinationChain, opts?.contractAddress); + quotes.push({ providerId, result: q }); + + if (q.simulatedError) { + failedProviders.push(providerId); + continue; + } + + return { + activeProviderId: providerId, + failedProviders, + attemptedProviders, + quotes, + durationMs: Date.now() - start, + }; + } + + return { + activeProviderId: null, + failedProviders, + attemptedProviders, + quotes, + durationMs: Date.now() - start, + }; + } + + reset(): void { + this.callCounts.clear(); + for (const p of this.providers) this.callCounts.set(p.id, 0); + } +} + +export interface OutageScenario { + id: string; + request: { + sourceChain: string; + destinationChain: string; + }; + routeId: string; + providers: SimulatorProvider[]; + priority: string[]; + /** Expected active provider after failover. */ + expectedActiveProviderId: string | null; + /** Providers that must be marked failed. */ + expectedFailedProviders: string[]; +} + +export function buildFailoverScenarios(): OutageScenario[] { + return [ + { + id: 'primary_unavailable_failover', + routeId: 'r-primary', + request: { sourceChain: 'Stellar', destinationChain: 'Ethereum' }, + providers: [ + { + id: 'AllBridge', + reliability: 0.97, + latencyMs: 4200, + feeBase: 1.5, + failure: { providerId: 'AllBridge', failAfterCalls: 0, reason: 'unavailable' }, + }, + { id: 'Wormhole', reliability: 0.95, latencyMs: 5100, feeBase: 1.2 }, + ], + priority: ['AllBridge', 'Wormhole'], + expectedActiveProviderId: 'Wormhole', + expectedFailedProviders: ['AllBridge'], + }, + { + id: 'primary_two_calls_then_fail', + routeId: 'r-primary', + request: { sourceChain: 'Stellar', destinationChain: 'Ethereum' }, + providers: [ + { + id: 'AllBridge', + reliability: 0.97, + latencyMs: 4200, + feeBase: 1.5, + failure: { providerId: 'AllBridge', failAfterCalls: 1, reason: 'timeout' }, + }, + { id: 'Wormhole', reliability: 0.95, latencyMs: 5100, feeBase: 1.2 }, + ], + priority: ['AllBridge', 'Wormhole'], + expectedActiveProviderId: 'Wormhole', + expectedFailedProviders: ['AllBridge'], + }, + { + id: 'all_providers_down', + routeId: 'r-primary', + request: { sourceChain: 'Stellar', destinationChain: 'Ethereum' }, + providers: [ + { id: 'AllBridge', reliability: 0.97, latencyMs: 4200, feeBase: 1.5, failure: { providerId: 'AllBridge', failAfterCalls: 0, reason: 'unavailable' } }, + { id: 'Wormhole', reliability: 0.95, latencyMs: 5100, feeBase: 1.2, failure: { providerId: 'Wormhole', failAfterCalls: 0, reason: 'unavailable' } }, + ], + priority: ['AllBridge', 'Wormhole'], + expectedActiveProviderId: null, + expectedFailedProviders: ['AllBridge', 'Wormhole'], + }, + ]; +} +