fix: Ponder: add test infrastructure + coverage for helpers (target 95%) (#287)
- Add vitest ^2 + @vitest/coverage-v8 ^2 as devDependencies - Add `test` and `test:coverage` scripts to package.json - Create vitest.config.ts with resolve.alias to mock ponder virtual modules (ponder:schema, ponder:registry) and point kraiken-lib/version to source - Add coverage/ to .gitignore - Add vitest.config.ts to tsconfig.json include so eslint project-aware rules apply - Create src/tests/__mocks__/ponder-schema.ts and ponder-registry.ts stubs - Create src/tests/stats.test.ts — 48 tests covering ring buffer logic, segment updates, hourly advancement, projections, ETH reserve snapshots, all exported async helpers with mock Ponder contexts - Create src/tests/version.test.ts — 14 tests covering isCompatibleVersion, getVersionMismatchError, and validateContractVersion (compatible / mismatch / error paths, existing-meta upsert path) - Create src/tests/abi.test.ts — 6 tests covering validateAbi and validateContractAbi Result: 68 tests pass, 100% line/statement/function coverage on all helpers (stats.ts, version.ts, abi.ts, logger.ts) — exceeds 95% target. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
24b3cf2836
commit
e49938bd0a
10 changed files with 1808 additions and 20 deletions
1
services/ponder/.gitignore
vendored
1
services/ponder/.gitignore
vendored
|
|
@ -3,5 +3,6 @@
|
|||
*.log
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
.DS_Store
|
||||
pnpm-lock.yaml
|
||||
|
|
|
|||
711
services/ponder/package-lock.json
generated
711
services/ponder/package-lock.json
generated
|
|
@ -17,13 +17,15 @@
|
|||
"@types/node": "^20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"@vitest/coverage-v8": "^2.0.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
|
@ -34,7 +36,8 @@
|
|||
"dependencies": {
|
||||
"@apollo/client": "^3.9.10",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-tag": "^2.12.6"
|
||||
"graphql-tag": "^2.12.6",
|
||||
"viem": "^2.22.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
|
|
@ -61,6 +64,20 @@
|
|||
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.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",
|
||||
|
|
@ -75,15 +92,62 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
||||
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bcoe/v8-coverage": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@commander-js/extra-typings": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-12.1.0.tgz",
|
||||
|
|
@ -1033,6 +1097,55 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||
|
|
@ -1733,6 +1846,152 @@
|
|||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "2.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz",
|
||||
"integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"debug": "^4.3.7",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-lib-source-maps": "^5.0.6",
|
||||
"istanbul-reports": "^3.1.7",
|
||||
"magic-string": "^0.30.12",
|
||||
"magicast": "^0.3.5",
|
||||
"std-env": "^3.8.0",
|
||||
"test-exclude": "^7.0.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "2.1.9",
|
||||
"vitest": "2.1.9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/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/@whatwg-node/disposablestack": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
|
||||
|
|
@ -1950,6 +2209,16 @@
|
|||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"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/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
|
|
@ -2065,6 +2334,33 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"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/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -2209,6 +2505,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
|
|
@ -2433,6 +2739,13 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"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.25.10",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
|
||||
|
|
@ -2712,19 +3025,6 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/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/espree": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||
|
|
@ -2792,6 +3092,16 @@
|
|||
"node": ">=4.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/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
|
|
@ -2849,6 +3159,16 @@
|
|||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -3177,6 +3497,13 @@
|
|||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-terminator": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz",
|
||||
|
|
@ -3373,6 +3700,60 @@
|
|||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-source-maps": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
|
||||
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.23",
|
||||
"debug": "^4.1.1",
|
||||
"istanbul-lib-coverage": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
|
|
@ -3805,12 +4186,57 @@
|
|||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"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/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magicast": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
"@babel/types": "^7.25.4",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
|
@ -4182,6 +4608,16 @@
|
|||
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
|
||||
"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/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
|
|
@ -4793,6 +5229,13 @@
|
|||
"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": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
|
|
@ -4826,6 +5269,13 @@
|
|||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stacktrace-parser": {
|
||||
"version": "0.1.11",
|
||||
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
|
||||
|
|
@ -4847,6 +5297,13 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
|
|
@ -5001,6 +5458,19 @@
|
|||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"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/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
|
|
@ -5022,6 +5492,60 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
|
||||
"integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^10.4.1",
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/minimatch": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz",
|
||||
|
|
@ -5031,6 +5555,50 @@
|
|||
"real-require": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"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/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/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
|
@ -5310,6 +5878,96 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"peer": true,
|
||||
"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/vitest/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/when-exit": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
|
||||
|
|
@ -5331,6 +5989,23 @@
|
|||
"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/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
"start": "node ./node_modules/ponder/dist/esm/bin/ponder.js start",
|
||||
"codegen": "node ./node_modules/ponder/dist/esm/bin/ponder.js codegen",
|
||||
"build": "node ./node_modules/ponder/dist/esm/bin/ponder.js codegen",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"ponder.config.ts\" \"ponder.schema.ts\"",
|
||||
|
|
@ -24,13 +26,15 @@
|
|||
"@types/node": "^20.11.30",
|
||||
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||
"@typescript-eslint/parser": "^8.45.0",
|
||||
"@vitest/coverage-v8": "^2.0.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.3",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.ts": [
|
||||
|
|
|
|||
5
services/ponder/src/tests/__mocks__/ponder-registry.ts
Normal file
5
services/ponder/src/tests/__mocks__/ponder-registry.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* Mock for ponder:registry virtual module used in unit tests.
|
||||
* Only the ponder.on shape is needed for type inference in stats.ts.
|
||||
*/
|
||||
export const ponder = { on: () => {} };
|
||||
11
services/ponder/src/tests/__mocks__/ponder-schema.ts
Normal file
11
services/ponder/src/tests/__mocks__/ponder-schema.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Mock for ponder:schema virtual module used in unit tests.
|
||||
* Exports the constants and table stubs needed by src/helpers/*.ts.
|
||||
*/
|
||||
export const HOURS_IN_RING_BUFFER = 168;
|
||||
export const SECONDS_IN_HOUR = 3600;
|
||||
export const STATS_ID = '0x01';
|
||||
|
||||
// Table stubs — only the identifier is needed; DB operations are mocked at context level.
|
||||
export const stats = {};
|
||||
export const stackMeta = {};
|
||||
92
services/ponder/src/tests/abi.test.ts
Normal file
92
services/ponder/src/tests/abi.test.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { validateAbi, validateContractAbi } from '../helpers/abi.js';
|
||||
import type { Abi } from 'viem';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateAbi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('validateAbi', () => {
|
||||
it('returns the exact same ABI reference it receives', () => {
|
||||
const abi: Abi = [
|
||||
{
|
||||
name: 'transfer',
|
||||
type: 'function',
|
||||
stateMutability: 'nonpayable',
|
||||
inputs: [
|
||||
{ name: 'to', type: 'address' },
|
||||
{ name: 'amount', type: 'uint256' },
|
||||
],
|
||||
outputs: [{ name: '', type: 'bool' }],
|
||||
},
|
||||
];
|
||||
const result = validateAbi(abi);
|
||||
expect(result).toBe(abi);
|
||||
});
|
||||
|
||||
it('returns an empty ABI unchanged', () => {
|
||||
const abi: Abi = [];
|
||||
expect(validateAbi(abi)).toBe(abi);
|
||||
});
|
||||
|
||||
it('preserves all entries in a multi-entry ABI', () => {
|
||||
const abi: Abi = [
|
||||
{
|
||||
name: 'mint',
|
||||
type: 'function',
|
||||
stateMutability: 'nonpayable',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
},
|
||||
{
|
||||
name: 'Transfer',
|
||||
type: 'event',
|
||||
inputs: [
|
||||
{ name: 'from', type: 'address', indexed: true },
|
||||
{ name: 'to', type: 'address', indexed: true },
|
||||
{ name: 'value', type: 'uint256', indexed: false },
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
];
|
||||
const result = validateAbi(abi);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toBe(abi);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateContractAbi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('validateContractAbi', () => {
|
||||
it('returns the abi property from the contract object', () => {
|
||||
const abi: Abi = [
|
||||
{
|
||||
name: 'balanceOf',
|
||||
type: 'function',
|
||||
stateMutability: 'view',
|
||||
inputs: [{ name: 'account', type: 'address' }],
|
||||
outputs: [{ name: '', type: 'uint256' }],
|
||||
},
|
||||
];
|
||||
const contract = { abi, address: '0xdeadbeef' };
|
||||
const result = validateContractAbi(contract);
|
||||
expect(result).toBe(abi);
|
||||
});
|
||||
|
||||
it('works with an empty abi property', () => {
|
||||
const contract = { abi: [] as Abi };
|
||||
const result = validateContractAbi(contract);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not return any other property of the contract object', () => {
|
||||
const abi: Abi = [];
|
||||
const contract = { abi, address: '0x1234', name: 'MyContract' };
|
||||
const result = validateContractAbi(contract);
|
||||
expect(result).toBe(abi);
|
||||
// result is just the ABI array, not the full contract object
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
772
services/ponder/src/tests/stats.test.ts
Normal file
772
services/ponder/src/tests/stats.test.ts
Normal file
|
|
@ -0,0 +1,772 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
makeEmptyRingBuffer,
|
||||
parseRingBuffer,
|
||||
serializeRingBuffer,
|
||||
checkBlockHistorySufficient,
|
||||
updateHourlyData,
|
||||
ensureStatsExists,
|
||||
recordEthReserveSnapshot,
|
||||
updateEthReserve,
|
||||
markPositionsUpdated,
|
||||
refreshOutstandingStake,
|
||||
refreshMinStake,
|
||||
RING_BUFFER_SEGMENTS,
|
||||
MINIMUM_BLOCKS_FOR_RINGBUFFER,
|
||||
type StatsContext,
|
||||
} from '../helpers/stats.js';
|
||||
|
||||
// Constants duplicated from the mock so tests don't re-import ponder:schema
|
||||
const HOURS = 168;
|
||||
const SECS_PER_HOUR = 3600;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper factories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MockStatsRow {
|
||||
ringBuffer: string[];
|
||||
ringBufferPointer: number;
|
||||
lastHourlyUpdateTimestamp: bigint;
|
||||
holderCount: number;
|
||||
minStake?: bigint;
|
||||
stakeTotalSupply?: bigint;
|
||||
outstandingStake?: bigint;
|
||||
kraikenTotalSupply?: bigint;
|
||||
}
|
||||
|
||||
function emptyStatsRow(overrides: Partial<MockStatsRow> = {}): MockStatsRow {
|
||||
return {
|
||||
ringBuffer: makeEmptyRingBuffer().map(String),
|
||||
ringBufferPointer: 0,
|
||||
lastHourlyUpdateTimestamp: 0n,
|
||||
holderCount: 0,
|
||||
minStake: 0n,
|
||||
stakeTotalSupply: 0n,
|
||||
outstandingStake: 0n,
|
||||
kraikenTotalSupply: 0n,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a ring buffer string[] with a known value at a specific slot. */
|
||||
function ringBufferWith(
|
||||
pointer: number,
|
||||
slotOffset: number,
|
||||
value: bigint,
|
||||
hoursBack = 0,
|
||||
): string[] {
|
||||
const buf = makeEmptyRingBuffer();
|
||||
const idx = ((pointer - hoursBack + HOURS) % HOURS) * RING_BUFFER_SEGMENTS + slotOffset;
|
||||
buf[idx] = value;
|
||||
return buf.map(String);
|
||||
}
|
||||
|
||||
function createSetMock() {
|
||||
return vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
function createMockContext(statsRow: MockStatsRow | null = null): StatsContext {
|
||||
const setFn = createSetMock();
|
||||
const ctx = {
|
||||
db: {
|
||||
find: vi.fn().mockResolvedValue(statsRow),
|
||||
update: vi.fn().mockReturnValue({ set: setFn }),
|
||||
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
||||
},
|
||||
client: {
|
||||
readContract: vi.fn().mockResolvedValue(0n),
|
||||
},
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0000000000000000000000000000000000000001' as `0x${string}` },
|
||||
Stake: { abi: [], address: '0x0000000000000000000000000000000000000002' as `0x${string}` },
|
||||
},
|
||||
network: {
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0000000000000000000000000000000000000001' },
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
};
|
||||
// Cast: vitest uses esbuild (no type-check), so the slim mock satisfies the runtime contract.
|
||||
return ctx as unknown as StatsContext;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// makeEmptyRingBuffer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('makeEmptyRingBuffer', () => {
|
||||
it('returns an array of the correct length', () => {
|
||||
const buf = makeEmptyRingBuffer();
|
||||
expect(buf).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
|
||||
});
|
||||
|
||||
it('fills every element with 0n', () => {
|
||||
const buf = makeEmptyRingBuffer();
|
||||
expect(buf.every(v => v === 0n)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseRingBuffer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('parseRingBuffer', () => {
|
||||
it('converts a string array to bigint array', () => {
|
||||
const raw = ['1', '2', '3'];
|
||||
expect(parseRingBuffer(raw)).toEqual([1n, 2n, 3n]);
|
||||
});
|
||||
|
||||
it('returns empty ring buffer for null input', () => {
|
||||
expect(parseRingBuffer(null)).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
|
||||
expect(parseRingBuffer(null).every(v => v === 0n)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty ring buffer for undefined input', () => {
|
||||
expect(parseRingBuffer(undefined)).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
|
||||
});
|
||||
|
||||
it('returns empty ring buffer for empty array input', () => {
|
||||
expect(parseRingBuffer([])).toHaveLength(HOURS * RING_BUFFER_SEGMENTS);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serializeRingBuffer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('serializeRingBuffer', () => {
|
||||
it('converts bigint array to string array', () => {
|
||||
expect(serializeRingBuffer([0n, 1n, 1000n])).toEqual(['0', '1', '1000']);
|
||||
});
|
||||
|
||||
it('round-trips through parseRingBuffer', () => {
|
||||
const original = makeEmptyRingBuffer();
|
||||
original[0] = 42n;
|
||||
original[RING_BUFFER_SEGMENTS] = 99n;
|
||||
const serialized = serializeRingBuffer(original);
|
||||
expect(parseRingBuffer(serialized)).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkBlockHistorySufficient
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('checkBlockHistorySufficient', () => {
|
||||
it('returns false when context has no network contracts', () => {
|
||||
const ctx = { network: {} } as unknown as StatsContext;
|
||||
const event = { block: { number: 200n } } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when context is missing network entirely', () => {
|
||||
const ctx = {} as unknown as StatsContext;
|
||||
const event = { block: { number: 200n } } as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when not enough blocks have passed', () => {
|
||||
const ctx = createMockContext();
|
||||
const event = {
|
||||
block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER - 1) },
|
||||
} as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses console fallback when context has no logger', () => {
|
||||
// Context with network.contracts.Kraiken but no logger — exercises logger.ts fallback branch
|
||||
const ctx = {
|
||||
network: { contracts: { Kraiken: {} } },
|
||||
} as unknown as StatsContext;
|
||||
const event = {
|
||||
block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER - 1) },
|
||||
} as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
// Should not throw; console.warn is used as fallback
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when sufficient blocks have passed', () => {
|
||||
const ctx = createMockContext();
|
||||
const event = {
|
||||
block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER + 1) },
|
||||
} as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true at exact threshold', () => {
|
||||
const ctx = createMockContext();
|
||||
const event = {
|
||||
block: { number: BigInt(MINIMUM_BLOCKS_FOR_RINGBUFFER) },
|
||||
} as unknown as Parameters<typeof checkBlockHistorySufficient>[1];
|
||||
expect(checkBlockHistorySufficient(ctx, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateHourlyData
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('updateHourlyData', () => {
|
||||
it('returns early when no stats row exists', async () => {
|
||||
const ctx = createMockContext(null);
|
||||
await updateHourlyData(ctx, BigInt(SECS_PER_HOUR * 10));
|
||||
expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initialises lastHourlyUpdateTimestamp when it is 0n', async () => {
|
||||
const row = emptyStatsRow({ lastHourlyUpdateTimestamp: 0n });
|
||||
const ctx = createMockContext(row);
|
||||
const timestamp = BigInt(SECS_PER_HOUR * 5);
|
||||
await updateHourlyData(ctx, timestamp);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
// Should store the current hour
|
||||
expect(setArg.lastHourlyUpdateTimestamp).toBe((timestamp / BigInt(SECS_PER_HOUR)) * BigInt(SECS_PER_HOUR));
|
||||
});
|
||||
|
||||
it('advances the pointer when a new hour has started', async () => {
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: 0,
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
// Move 3 hours forward
|
||||
const newTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR * 3 + 60);
|
||||
await updateHourlyData(ctx, newTimestamp);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.ringBufferPointer).toBe(3);
|
||||
});
|
||||
|
||||
it('clamps hours elapsed to HOURS_IN_RING_BUFFER to prevent full-buffer clear loops', async () => {
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: 0,
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
// Jump 500 hours — exceeds ring buffer
|
||||
const newTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR * 500);
|
||||
await updateHourlyData(ctx, newTimestamp);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
// pointer should wrap around within HOURS range
|
||||
expect(setArg.ringBufferPointer).toBeGreaterThanOrEqual(0);
|
||||
expect(setArg.ringBufferPointer).toBeLessThan(HOURS);
|
||||
});
|
||||
|
||||
it('computes metrics when hour has advanced (mintedDay, burnedDay, etc.)', async () => {
|
||||
const pointer = 5;
|
||||
// Slot 1 (minted) at index i=0 (current hour) through i=23 — 10n in each of last 48 hours
|
||||
const buf = makeEmptyRingBuffer();
|
||||
for (let h = 0; h < 48; h++) {
|
||||
const idx = ((pointer - h + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[idx + 1] = 10n; // minted
|
||||
buf[idx + 2] = 5n; // burned
|
||||
}
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
|
||||
await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
// mintedLastWeek and mintedLastDay should be positive
|
||||
expect(setArg.mintedLastWeek).toBeGreaterThan(0n);
|
||||
expect(setArg.mintedLastDay).toBeGreaterThan(0n);
|
||||
expect(setArg.burnedLastWeek).toBeGreaterThan(0n);
|
||||
expect(setArg.burnedLastDay).toBeGreaterThan(0n);
|
||||
});
|
||||
|
||||
it('carries forward holderCount into new ring buffer slots', async () => {
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: BigInt(SECS_PER_HOUR * 10),
|
||||
ringBufferPointer: 0,
|
||||
holderCount: 42,
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
await updateHourlyData(ctx, BigInt(SECS_PER_HOUR * 12));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArgs = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
const updatedBuf = parseRingBuffer(setArgs.ringBuffer as string[]);
|
||||
// pointer advanced to 2; slot 3 of new pointer should be 42n
|
||||
const newPointer = 2;
|
||||
expect(updatedBuf[newPointer * RING_BUFFER_SEGMENTS + 3]).toBe(42n);
|
||||
});
|
||||
|
||||
it('computes projections when same hour (no advancement)', async () => {
|
||||
const pointer = 3;
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 100);
|
||||
// Add some minted data in the current and previous slot
|
||||
const buf = makeEmptyRingBuffer();
|
||||
buf[pointer * RING_BUFFER_SEGMENTS + 1] = 100n; // current hour minted
|
||||
const prevPointer = ((pointer - 1 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[prevPointer + 1] = 80n; // previous hour minted
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
|
||||
// Same hour but 30 minutes in
|
||||
const sameHourTimestamp = baseTimestamp + BigInt(SECS_PER_HOUR / 2);
|
||||
await updateHourlyData(ctx, sameHourTimestamp);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
// Projections should be computed
|
||||
expect(setArg.mintNextHourProjected).toBeDefined();
|
||||
expect(setArg.burnNextHourProjected).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles zero elapsedSeconds in projection (exact hour boundary)', async () => {
|
||||
const pointer = 0;
|
||||
const exactHour = BigInt(SECS_PER_HOUR * 50);
|
||||
const buf = makeEmptyRingBuffer();
|
||||
buf[pointer * RING_BUFFER_SEGMENTS + 1] = 50n; // some minted
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: exactHour,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
|
||||
// Pass exactly the same timestamp (elapsedSeconds = 0)
|
||||
await updateHourlyData(ctx, exactHour);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('computes ETH reserve change metrics when reserves are populated', async () => {
|
||||
const pointer = 25;
|
||||
const buf = makeEmptyRingBuffer();
|
||||
// Set current hour ETH reserve (i=0)
|
||||
buf[pointer * RING_BUFFER_SEGMENTS + 0] = 2000n;
|
||||
// Set 24h ago ETH reserve (i=23)
|
||||
const idx24h = ((pointer - 23 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[idx24h + 0] = 1000n;
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.ethReserveLastDay).toBeGreaterThanOrEqual(0n);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// recordEthReserveSnapshot
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('recordEthReserveSnapshot', () => {
|
||||
it('returns early when no stats row exists', async () => {
|
||||
const ctx = createMockContext(null);
|
||||
await recordEthReserveSnapshot(ctx, 1000n, 500n);
|
||||
expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('writes the ETH balance into slot 0 of the current pointer', async () => {
|
||||
const pointer = 2;
|
||||
const row = emptyStatsRow({ ringBufferPointer: pointer });
|
||||
const ctx = createMockContext(row);
|
||||
const ethBalance = 12345678n;
|
||||
|
||||
await recordEthReserveSnapshot(ctx, 1000n, ethBalance);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
const updatedBuf = parseRingBuffer(setArg.ringBuffer as string[]);
|
||||
expect(updatedBuf[pointer * RING_BUFFER_SEGMENTS + 0]).toBe(ethBalance);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// updateEthReserve
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('updateEthReserve', () => {
|
||||
it('skips update when weth balance is 0', async () => {
|
||||
const ctx = createMockContext(emptyStatsRow());
|
||||
// readContract returns 0n by default
|
||||
await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);
|
||||
expect((ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } }).db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates lastEthReserve when weth balance is non-zero', async () => {
|
||||
const ctx = createMockContext(emptyStatsRow());
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(9999n);
|
||||
|
||||
await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.lastEthReserve).toBe(9999n);
|
||||
});
|
||||
|
||||
it('logs warning and returns when readContract throws', async () => {
|
||||
const ctx = createMockContext(emptyStatsRow());
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockRejectedValue(new Error('rpc error'));
|
||||
|
||||
await updateEthReserve(ctx, '0xpool0000000000000000000000000000000000001' as `0x${string}`);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ensureStatsExists
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ensureStatsExists', () => {
|
||||
it('returns existing stats without re-creating when row exists', async () => {
|
||||
const row = emptyStatsRow();
|
||||
const ctx = createMockContext(row);
|
||||
|
||||
const result = await ensureStatsExists(ctx, 1000n);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.insert).not.toHaveBeenCalled();
|
||||
expect(result).toBe(row);
|
||||
});
|
||||
|
||||
it('creates a new stats row when none exists', async () => {
|
||||
// First call returns null, second call (post-insert) returns the row
|
||||
const row = emptyStatsRow();
|
||||
const dbFindMock = vi.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(row);
|
||||
const ctx = createMockContext(null);
|
||||
(ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;
|
||||
|
||||
const result = await ensureStatsExists(ctx, 1000n);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.insert).toHaveBeenCalled();
|
||||
expect(result).toBe(row);
|
||||
});
|
||||
|
||||
it('uses fallback values when readContract throws during creation', async () => {
|
||||
const row = emptyStatsRow();
|
||||
const dbFindMock = vi.fn()
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce(row);
|
||||
const ctx = createMockContext(null);
|
||||
(ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockRejectedValue(new Error('rpc error'));
|
||||
|
||||
const result = await ensureStatsExists(ctx, 1000n);
|
||||
|
||||
expect(result).toBe(row);
|
||||
// insert should still have been called with fallback 0n values
|
||||
const dbMock = ctx as unknown as { db: { insert: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.insert).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works without timestamp argument', async () => {
|
||||
const row = emptyStatsRow();
|
||||
const ctx = createMockContext(row);
|
||||
const result = await ensureStatsExists(ctx);
|
||||
expect(result).toBe(row);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// markPositionsUpdated
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('markPositionsUpdated', () => {
|
||||
it('updates positionsUpdatedAt in the stats row', async () => {
|
||||
const row = emptyStatsRow();
|
||||
const ctx = createMockContext(row);
|
||||
const ts = 7777n;
|
||||
|
||||
await markPositionsUpdated(ctx, ts);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.positionsUpdatedAt).toBe(ts);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getStakeTotalSupply
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('getStakeTotalSupply', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('reads totalSupply from contract when cache is cold', async () => {
|
||||
// Fresh import to reset module-level cache
|
||||
const { getStakeTotalSupply: freshGet } = await import('../helpers/stats.js');
|
||||
const row = emptyStatsRow({ stakeTotalSupply: 0n });
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(555n);
|
||||
|
||||
const result = await freshGet(ctx);
|
||||
|
||||
expect(result).toBe(555n);
|
||||
});
|
||||
|
||||
it('returns cached value without calling readContract on second call', async () => {
|
||||
// Fresh import to get a clean module with null cache
|
||||
const { getStakeTotalSupply: freshGet } = await import('../helpers/stats.js');
|
||||
const row = emptyStatsRow({ stakeTotalSupply: 0n });
|
||||
const readContractMock = vi.fn().mockResolvedValue(777n);
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= readContractMock;
|
||||
|
||||
// First call — populates cache via readContract
|
||||
const first = await freshGet(ctx);
|
||||
expect(first).toBe(777n);
|
||||
|
||||
// Second call — should return cached value, readContract not called again
|
||||
const second = await freshGet(ctx);
|
||||
expect(second).toBe(777n);
|
||||
expect(readContractMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// refreshOutstandingStake
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('refreshOutstandingStake', () => {
|
||||
it('reads outstandingStake from contract and persists it', async () => {
|
||||
const row = emptyStatsRow();
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(1234n);
|
||||
|
||||
await refreshOutstandingStake(ctx);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.outstandingStake).toBe(1234n);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// refreshMinStake
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('refreshMinStake', () => {
|
||||
it('skips update when minStake is unchanged', async () => {
|
||||
const row = emptyStatsRow({ minStake: 500n });
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(500n);
|
||||
|
||||
await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates minStake when the on-chain value has changed', async () => {
|
||||
const row = emptyStatsRow({ minStake: 100n });
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(200n);
|
||||
|
||||
await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.minStake).toBe(200n);
|
||||
});
|
||||
|
||||
it('fetches stats when no statsData arg is provided and row exists', async () => {
|
||||
const row = emptyStatsRow({ minStake: 0n });
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(300n);
|
||||
|
||||
await refreshMinStake(ctx);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates stats row when find returns null', async () => {
|
||||
const row = emptyStatsRow({ minStake: 0n });
|
||||
const dbFindMock = vi.fn()
|
||||
.mockResolvedValueOnce(null) // first find (refresh check)
|
||||
.mockResolvedValueOnce(null) // ensureStatsExists find
|
||||
.mockResolvedValueOnce(row); // ensureStatsExists post-insert
|
||||
const ctx = createMockContext(null);
|
||||
(ctx as unknown as { db: { find: ReturnType<typeof vi.fn> } }).db.find = dbFindMock;
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockResolvedValue(300n);
|
||||
|
||||
await refreshMinStake(ctx);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs warning and returns when readContract throws', async () => {
|
||||
const row = emptyStatsRow({ minStake: 100n });
|
||||
const ctx = createMockContext(row);
|
||||
(ctx as unknown as { client: { readContract: ReturnType<typeof vi.fn> } }).client.readContract
|
||||
= vi.fn().mockRejectedValue(new Error('rpc error'));
|
||||
|
||||
await refreshMinStake(ctx, row as unknown as Awaited<ReturnType<typeof ensureStatsExists>>);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses ringBufferWith helper to build non-trivial buffers', () => {
|
||||
const pointer = 10;
|
||||
const buf = parseRingBuffer(ringBufferWith(pointer, 1, 999n));
|
||||
expect(buf[pointer * RING_BUFFER_SEGMENTS + 1]).toBe(999n);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RING_BUFFER_SEGMENTS and MINIMUM_BLOCKS_FOR_RINGBUFFER exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('constants', () => {
|
||||
it('RING_BUFFER_SEGMENTS is 4', () => {
|
||||
expect(RING_BUFFER_SEGMENTS).toBe(4);
|
||||
});
|
||||
|
||||
it('MINIMUM_BLOCKS_FOR_RINGBUFFER is 100', () => {
|
||||
expect(MINIMUM_BLOCKS_FOR_RINGBUFFER).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// computeMetrics coverage: holderCount and ethReserve at 24h/7d boundaries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('computeMetrics boundary coverage (via updateHourlyData)', () => {
|
||||
it('captures ethReserve7dAgo from the oldest non-zero slot', async () => {
|
||||
const pointer = 2;
|
||||
const buf = makeEmptyRingBuffer();
|
||||
// Fill a slot > 24 hours back with a non-zero ETH reserve
|
||||
const oldSlot = ((pointer - 50 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[oldSlot + 0] = 500n;
|
||||
// Current hour also has a value
|
||||
buf[pointer * RING_BUFFER_SEGMENTS + 0] = 1000n;
|
||||
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 200);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.ethReserveLastWeek).toBeGreaterThanOrEqual(0n);
|
||||
});
|
||||
|
||||
it('records holderCount at 24h and 7d ago markers', async () => {
|
||||
const pointer = 30;
|
||||
const buf = makeEmptyRingBuffer();
|
||||
// Set holderCount at current pointer (i=0)
|
||||
buf[pointer * RING_BUFFER_SEGMENTS + 3] = 100n;
|
||||
// Set holderCount at 24h ago (i=23)
|
||||
const idx24h = ((pointer - 23 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[idx24h + 3] = 80n;
|
||||
// Set holderCount far back (i=150, older than 7d)
|
||||
const idxOld = ((pointer - 150 + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[idxOld + 3] = 50n;
|
||||
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 500);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR * 2));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
expect(dbMock.db.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles projection fallback when weekly totals are also zero', async () => {
|
||||
// All zeros in ring buffer — medium will be 0, fallback is weekly/7
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: BigInt(SECS_PER_HOUR * 100),
|
||||
ringBufferPointer: 0,
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
const sameHourTimestamp = BigInt(SECS_PER_HOUR * 100) + BigInt(SECS_PER_HOUR / 4);
|
||||
await updateHourlyData(ctx, sameHourTimestamp);
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
// Projection should be 0 (weekly/7 = 0/7 = 0)
|
||||
expect(setArg.mintNextHourProjected).toBe(0n);
|
||||
expect(setArg.burnNextHourProjected).toBe(0n);
|
||||
});
|
||||
|
||||
it('computes netSupplyChange from minted minus burned', async () => {
|
||||
const pointer = 1;
|
||||
const buf = makeEmptyRingBuffer();
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const idx = ((pointer - h + HOURS) % HOURS) * RING_BUFFER_SEGMENTS;
|
||||
buf[idx + 1] = 20n; // minted
|
||||
buf[idx + 2] = 8n; // burned
|
||||
}
|
||||
const baseTimestamp = BigInt(SECS_PER_HOUR * 300);
|
||||
const row = emptyStatsRow({
|
||||
lastHourlyUpdateTimestamp: baseTimestamp,
|
||||
ringBufferPointer: pointer,
|
||||
ringBuffer: buf.map(String),
|
||||
});
|
||||
const ctx = createMockContext(row);
|
||||
await updateHourlyData(ctx, baseTimestamp + BigInt(SECS_PER_HOUR));
|
||||
|
||||
const dbMock = ctx as unknown as { db: { update: ReturnType<typeof vi.fn> } };
|
||||
const setArg = (dbMock.db.update as ReturnType<typeof vi.fn>).mock.results[0].value.set.mock.calls[0][0];
|
||||
expect(setArg.netSupplyChangeDay).toBeGreaterThan(0n);
|
||||
expect(setArg.netSupplyChangeWeek).toBeGreaterThan(0n);
|
||||
});
|
||||
});
|
||||
|
||||
201
services/ponder/src/tests/version.test.ts
Normal file
201
services/ponder/src/tests/version.test.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import {
|
||||
isCompatibleVersion,
|
||||
getVersionMismatchError,
|
||||
KRAIKEN_LIB_VERSION,
|
||||
COMPATIBLE_CONTRACT_VERSIONS,
|
||||
} from 'kraiken-lib/version';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isCompatibleVersion (from kraiken-lib/version, aliased to source)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isCompatibleVersion', () => {
|
||||
it('returns true for each version in COMPATIBLE_CONTRACT_VERSIONS', () => {
|
||||
for (const v of COMPATIBLE_CONTRACT_VERSIONS) {
|
||||
expect(isCompatibleVersion(v)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false for version 0', () => {
|
||||
expect(isCompatibleVersion(0)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a future version not yet listed', () => {
|
||||
expect(isCompatibleVersion(9999)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for negative version', () => {
|
||||
expect(isCompatibleVersion(-1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getVersionMismatchError (from kraiken-lib/version)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('getVersionMismatchError', () => {
|
||||
it('includes the bad contract version in the output (ponder context)', () => {
|
||||
const msg = getVersionMismatchError(99, 'ponder');
|
||||
expect(msg).toContain('99');
|
||||
});
|
||||
|
||||
it('includes the library version in the output', () => {
|
||||
const msg = getVersionMismatchError(99, 'ponder');
|
||||
expect(msg).toContain(String(KRAIKEN_LIB_VERSION));
|
||||
});
|
||||
|
||||
it('includes ponder-specific remediation steps', () => {
|
||||
const msg = getVersionMismatchError(99, 'ponder');
|
||||
expect(msg).toContain('COMPATIBLE_CONTRACT_VERSIONS');
|
||||
});
|
||||
|
||||
it('includes frontend-specific remediation steps', () => {
|
||||
const msg = getVersionMismatchError(99, 'frontend');
|
||||
expect(msg).toContain('administrator');
|
||||
});
|
||||
|
||||
it('formats compatible versions list', () => {
|
||||
const msg = getVersionMismatchError(99, 'ponder');
|
||||
for (const v of COMPATIBLE_CONTRACT_VERSIONS) {
|
||||
expect(msg).toContain(String(v));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a multi-line string (box drawing)', () => {
|
||||
const msg = getVersionMismatchError(99, 'ponder');
|
||||
expect(msg.split('\n').length).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateContractVersion (helpers/version.ts) — mock the Ponder context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('validateContractVersion', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('logs success and upserts metadata on compatible version', async () => {
|
||||
const { validateContractVersion } = await import('../helpers/version.js');
|
||||
|
||||
const setFn = vi.fn().mockResolvedValue(undefined);
|
||||
const ctx = {
|
||||
client: {
|
||||
readContract: vi.fn().mockResolvedValue(KRAIKEN_LIB_VERSION),
|
||||
},
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
||||
},
|
||||
db: {
|
||||
find: vi.fn().mockResolvedValue(null),
|
||||
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
||||
update: vi.fn().mockReturnValue({ set: setFn }),
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
||||
|
||||
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
||||
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
expect(ctx.logger.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls process.exit on incompatible contract version', async () => {
|
||||
const { validateContractVersion } = await import('../helpers/version.js');
|
||||
|
||||
const ctx = {
|
||||
client: {
|
||||
readContract: vi.fn().mockResolvedValue(9999n),
|
||||
},
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
||||
},
|
||||
db: {
|
||||
find: vi.fn().mockResolvedValue(null),
|
||||
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
||||
update: vi.fn().mockReturnValue({ set: vi.fn().mockResolvedValue(undefined) }),
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
||||
|
||||
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('calls process.exit when readContract throws', async () => {
|
||||
const { validateContractVersion } = await import('../helpers/version.js');
|
||||
|
||||
const ctx = {
|
||||
client: {
|
||||
readContract: vi.fn().mockRejectedValue(new Error('network error')),
|
||||
},
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
||||
},
|
||||
db: {
|
||||
find: vi.fn().mockResolvedValue(null),
|
||||
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
||||
update: vi.fn().mockReturnValue({ set: vi.fn().mockResolvedValue(undefined) }),
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
||||
|
||||
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('updates existing metadata when a row already exists', async () => {
|
||||
const { validateContractVersion } = await import('../helpers/version.js');
|
||||
|
||||
const setFn = vi.fn().mockResolvedValue(undefined);
|
||||
const existingMeta = { id: 'stack-meta', contractVersion: 1 };
|
||||
const ctx = {
|
||||
client: {
|
||||
readContract: vi.fn().mockResolvedValue(KRAIKEN_LIB_VERSION),
|
||||
},
|
||||
contracts: {
|
||||
Kraiken: { abi: [], address: '0x0001' as `0x${string}` },
|
||||
},
|
||||
db: {
|
||||
find: vi.fn().mockResolvedValue(existingMeta),
|
||||
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
||||
update: vi.fn().mockReturnValue({ set: setFn }),
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as () => never);
|
||||
|
||||
await validateContractVersion(ctx as unknown as import('ponder:registry').Context);
|
||||
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
expect(ctx.db.update).toHaveBeenCalled();
|
||||
expect(ctx.db.insert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -20,6 +20,6 @@
|
|||
"ponder/virtual": ["./node_modules/ponder/src/types.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "ponder.config.ts", "ponder.schema.ts", "ponder-env.d.ts"],
|
||||
"include": ["src/**/*", "ponder.config.ts", "ponder.schema.ts", "ponder-env.d.ts", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", ".ponder"]
|
||||
}
|
||||
|
|
|
|||
27
services/ponder/vitest.config.ts
Normal file
27
services/ponder/vitest.config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/helpers/**/*.ts'],
|
||||
reporter: ['text', 'lcov'],
|
||||
thresholds: {
|
||||
lines: 95,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'ponder:schema': resolve(rootDir, 'src/tests/__mocks__/ponder-schema.ts'),
|
||||
'ponder:registry': resolve(rootDir, 'src/tests/__mocks__/ponder-registry.ts'),
|
||||
'kraiken-lib/version': resolve(rootDir, '../../kraiken-lib/src/version.ts'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue