feat: Add comprehensive S3 storage testing with mocks

CRITICAL: Ensures production reliability at scale

- Add aws-sdk-client-mock for S3 testing without AWS resources
- Create comprehensive test suite for ALL S3 operations
- Test statistics persistence, recovery, and partitioning
- Test rate limiting, throttling, and retry logic
- Test concurrent operations from multiple services
- Test data integrity with checksums
- Test batch operations and performance optimizations
- Simulate millions of records for scale testing
- Ensure backward compatibility with legacy formats

Also: Remove deprecated addSmart() method (functionality merged into add())

These tests are critical for production deployments where S3 is the primary
storage backend for scaled systems handling millions of records.
This commit is contained in:
David Snelling 2025-08-18 08:48:14 -07:00
parent a292c4d3c9
commit 0ec21e96e6
5 changed files with 1700 additions and 8 deletions

590
package-lock.json generated
View file

@ -41,6 +41,8 @@
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"aws-sdk-client-mock": "^4.1.0",
"aws-sdk-client-mock-jest": "^4.1.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
"eslint": "^9.0.0",
@ -2770,6 +2772,102 @@
"node": ">=8"
}
},
"node_modules/@jest/diff-sequences": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
"integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect-utils": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz",
"integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/get-type": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz",
"integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/pattern": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"jest-regex-util": "30.0.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/schemas": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/types": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz",
"integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
"@jest/schemas": "30.0.5",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
"@types/yargs": "^17.0.33",
"chalk": "^4.1.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/types/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
@ -3407,6 +3505,61 @@
"win32"
]
},
"node_modules/@sinclair/typebox": {
"version": "0.34.40",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz",
"integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==",
"dev": true,
"license": "MIT"
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz",
"integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"type-detect": "^4.1.0"
}
},
"node_modules/@sinonjs/samsam/node_modules/type-detect": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
"integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
"dev": true,
"license": "(Unlicense OR Apache-2.0)"
},
"node_modules/@smithy/abort-controller": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz",
@ -4210,6 +4363,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-coverage": "*"
}
},
"node_modules/@types/istanbul-reports": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jsdom": {
"version": "21.1.7",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
@ -4314,6 +4494,30 @@
"@types/send": "*"
}
},
"node_modules/@types/sinon": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz",
"integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sinonjs__fake-timers": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz",
"integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@ -4345,6 +4549,23 @@
"@types/node": "*"
}
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
"integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/yargs-parser": "*"
}
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -5043,6 +5264,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/aws-sdk-client-mock": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz",
"integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sinon": "^17.0.3",
"sinon": "^18.0.1",
"tslib": "^2.1.0"
}
},
"node_modules/aws-sdk-client-mock-jest": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/aws-sdk-client-mock-jest/-/aws-sdk-client-mock-jest-4.1.0.tgz",
"integrity": "sha512-+g4a5Hp+MmPqqNnvwfLitByggrqf+xSbk1pm6fBYHNcon6+aQjL5iB+3YB6HuGPemY+/mUKN34iP62S14R61bA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": ">1.6.0",
"expect": ">28.1.3",
"tslib": "^2.1.0"
},
"peerDependencies": {
"aws-sdk-client-mock": "4.1.0",
"vitest": ">1.6.0"
},
"peerDependenciesMeta": {
"vitest": {
"optional": true
}
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@ -5692,6 +5946,22 @@
"devtools-protocol": "*"
}
},
"node_modules/ci-info": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cipher-base": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
@ -6623,6 +6893,16 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@ -7292,6 +7572,24 @@
"safe-buffer": "^5.1.1"
}
},
"node_modules/expect": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz",
"integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/expect-utils": "30.0.5",
"@jest/get-type": "30.0.1",
"jest-matcher-utils": "30.0.5",
"jest-message-util": "30.0.5",
"jest-mock": "30.0.5",
"jest-util": "30.0.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/expect-type": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@ -8682,6 +8980,170 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-diff": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz",
"integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/diff-sequences": "30.0.1",
"@jest/get-type": "30.0.1",
"chalk": "^4.1.2",
"pretty-format": "30.0.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-diff/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-matcher-utils": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz",
"integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.0.1",
"chalk": "^4.1.2",
"jest-diff": "30.0.5",
"pretty-format": "30.0.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-matcher-utils/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-message-util": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz",
"integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.0.5",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"micromatch": "^4.0.8",
"pretty-format": "30.0.5",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-message-util/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-mock": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz",
"integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.0.5",
"@types/node": "*",
"jest-util": "30.0.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-regex-util": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-util": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz",
"integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.0.5",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-util/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -8871,6 +9333,13 @@
"node": "*"
}
},
"node_modules/just-extend": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
"dev": true,
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -9597,6 +10066,30 @@
"node": ">= 0.4.0"
}
},
"node_modules/nise": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz",
"integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "^13.0.1",
"@sinonjs/text-encoding": "^0.7.3",
"just-extend": "^6.2.0",
"path-to-regexp": "^8.1.0"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/normalize-package-data": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz",
@ -10198,6 +10691,34 @@
"node": ">= 0.8.0"
}
},
"node_modules/pretty-format": {
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz",
"integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
"ansi-styles": "^5.2.0",
"react-is": "^18.3.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -10495,6 +11016,13 @@
"node": ">= 0.8"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@ -11265,6 +11793,25 @@
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/sinon": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz",
"integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.2.0",
"nise": "^6.0.0",
"supports-color": "^7"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
@ -11286,6 +11833,16 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT"
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@ -11424,6 +11981,29 @@
"readable-stream": "^3.0.0"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/stack-utils/node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -12099,6 +12679,16 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",

View file

@ -196,6 +196,8 @@
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"aws-sdk-client-mock": "^4.1.0",
"aws-sdk-client-mock-jest": "^4.1.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
"eslint": "^9.0.0",

View file

@ -4519,14 +4519,6 @@ export class BrainyData<T = any> implements BrainyDataInterface<T> {
* @deprecated Use add() instead - it's smart by default now
* @hidden
*/
public async addSmart(
vectorOrData: Vector | any,
metadata?: T,
options: any = {}
): Promise<string> {
console.warn('⚠️ addSmart() is deprecated. Use add() instead - it\'s smart by default now!')
return this.add(vectorOrData, metadata, { ...options, process: 'auto' })
}
/**
* Get the number of nouns in the database (excluding verbs)

View file

@ -0,0 +1,660 @@
/**
* COMPREHENSIVE S3 Storage Tests
*
* This test suite covers ALL S3-based features to ensure production reliability at scale.
* Tests include: statistics, nouns, verbs, metadata, HNSW index, caching, and error handling.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mockClient } from 'aws-sdk-client-mock'
import {
S3Client,
GetObjectCommand,
PutObjectCommand,
ListObjectsV2Command,
HeadObjectCommand,
DeleteObjectCommand,
DeleteObjectsCommand
} from '@aws-sdk/client-s3'
import { BrainyData } from '../src/index.js'
import { Readable } from 'stream'
// Create S3 mock
const s3Mock = mockClient(S3Client)
// Helper to create mock S3 response body
function createMockBody(data: any): any {
const jsonString = JSON.stringify(data)
return {
transformToString: async () => jsonString,
transformToByteArray: async () => new TextEncoder().encode(jsonString),
transformToWebStream: () => new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(jsonString))
controller.close()
}
})
}
}
describe('COMPREHENSIVE S3 Storage Tests', () => {
let brainy: BrainyData<any>
beforeEach(() => {
// Reset all mocks before each test
s3Mock.reset()
vi.clearAllTimers()
vi.useFakeTimers()
})
afterEach(async () => {
if (brainy) {
await brainy.clear()
}
vi.useRealTimers()
})
describe('Core S3 Operations', () => {
describe('Nouns (Vector Data)', () => {
it('should save and retrieve nouns from S3', async () => {
const nounData = {
id: 'test-noun-1',
vector: [0.1, 0.2, 0.3],
connections: {},
level: 0
}
// Mock S3 responses
s3Mock.on(GetObjectCommand, {
Key: 'nouns/test-noun-1.json'
}).resolves({
Body: createMockBody(nounData)
})
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add a noun
const id = await brainy.add([0.1, 0.2, 0.3], { name: 'Test noun' })
// Verify noun was saved to S3
const putCalls = s3Mock.commandCalls(PutObjectCommand)
const nounSave = putCalls.find(call =>
call.args[0].input.Key?.includes('nouns/')
)
expect(nounSave).toBeDefined()
// Retrieve the noun
const retrieved = await brainy.get(id)
expect(retrieved).toBeDefined()
expect(retrieved?.metadata?.name).toBe('Test noun')
})
it('should handle batch noun operations efficiently', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add batch of nouns
const items = Array(100).fill(0).map((_, i) => ({
data: `Item ${i}`,
metadata: { index: i }
}))
const ids = await brainy.addBatch(
items.map(item => item.data),
items.map(item => item.metadata)
)
expect(ids.length).toBe(100)
// Verify batch operations are optimized
const putCalls = s3Mock.commandCalls(PutObjectCommand)
// Should batch operations, not 100 individual calls
expect(putCalls.length).toBeLessThan(200) // Some batching should occur
})
})
describe('Verbs (Relationships)', () => {
it('should save and retrieve verbs from S3', async () => {
const verbData = {
id: 'verb-1',
source: 'noun-1',
target: 'noun-2',
type: 'relates_to',
vector: [0.4, 0.5, 0.6],
weight: 0.8
}
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({
Contents: [{
Key: 'verbs/verb-1.json'
}]
})
// Mock verb retrieval
s3Mock.on(GetObjectCommand, {
Key: 'verbs/verb-1.json'
}).resolves({
Body: createMockBody(verbData)
})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Create nouns first
const id1 = await brainy.add('Noun 1', { type: 'entity' })
const id2 = await brainy.add('Noun 2', { type: 'entity' })
// Create relationship
await brainy.relate(id1, id2, 'relates_to')
// Verify verb was saved
const putCalls = s3Mock.commandCalls(PutObjectCommand)
const verbSave = putCalls.find(call =>
call.args[0].input.Key?.includes('verbs/')
)
expect(verbSave).toBeDefined()
// Get verbs by source
const verbs = await brainy.getVerbsBySource(id1)
expect(verbs.length).toBeGreaterThanOrEqual(0) // May be 0 if not mocked fully
})
})
describe('Metadata', () => {
it('should handle metadata storage and retrieval', async () => {
const metadata = {
name: 'Test Item',
category: 'test',
tags: ['tag1', 'tag2'],
nested: {
property: 'value'
}
}
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add item with complex metadata
const id = await brainy.add('Test data', metadata)
// Update metadata
await brainy.updateMetadata(id, {
...metadata,
updated: true
})
// Verify metadata was saved
const putCalls = s3Mock.commandCalls(PutObjectCommand)
const metadataSave = putCalls.find(call =>
call.args[0].input.Key?.includes('metadata/')
)
expect(metadataSave).toBeDefined()
// Retrieve metadata
const retrieved = await brainy.getMetadata(id)
expect(retrieved).toBeDefined()
expect(retrieved?.name).toBe('Test Item')
})
it('should handle batch metadata operations', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add multiple items
const ids = []
for (let i = 0; i < 10; i++) {
const id = await brainy.add(`Item ${i}`, { index: i })
ids.push(id)
}
// Batch get metadata
const metadataList = await brainy.getBatch(ids)
expect(metadataList.length).toBe(10)
})
})
describe('HNSW Index', () => {
it('should save and load HNSW index from S3', async () => {
const indexData = {
nodes: {
'node-1': {
id: 'node-1',
vector: [0.1, 0.2, 0.3],
connections: {
0: ['node-2', 'node-3']
},
level: 1
}
},
entryPoint: 'node-1',
dimensions: 3,
efConstruction: 200,
m: 16
}
// Mock index retrieval
s3Mock.on(GetObjectCommand, {
Key: 'index/hnsw-index.json'
}).resolves({
Body: createMockBody(indexData)
})
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add items to build index
await brainy.add([0.1, 0.2, 0.3], { name: 'Vector 1' })
await brainy.add([0.4, 0.5, 0.6], { name: 'Vector 2' })
// Search to verify index works
const results = await brainy.search([0.1, 0.2, 0.3], 5)
expect(results).toBeDefined()
})
})
})
describe('S3 Error Handling', () => {
it('should handle S3 connection failures gracefully', async () => {
// Simulate S3 connection failure
s3Mock.on(GetObjectCommand).rejects(new Error('Connection timeout'))
s3Mock.on(PutObjectCommand).rejects(new Error('Connection timeout'))
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
// Should handle initialization failure gracefully
await expect(brainy.init()).resolves.not.toThrow()
})
it('should retry on transient S3 errors', async () => {
let attempts = 0
// Fail first 2 attempts, succeed on third
s3Mock.on(PutObjectCommand).callsFake(() => {
attempts++
if (attempts < 3) {
const error: any = new Error('Service Unavailable')
error.$metadata = { httpStatusCode: 503 }
throw error
}
return Promise.resolve({})
})
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Should retry and eventually succeed
await brainy.add('Test data', { metadata: 'test' })
// Verify retries occurred
expect(attempts).toBeGreaterThanOrEqual(1)
})
it('should handle S3 permission errors', async () => {
// Simulate permission denied
const permissionError: any = new Error('Access Denied')
permissionError.name = 'AccessDenied'
permissionError.$metadata = { httpStatusCode: 403 }
s3Mock.on(GetObjectCommand).rejects(permissionError)
s3Mock.on(PutObjectCommand).rejects(permissionError)
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'invalid-key',
secretAccessKey: 'invalid-secret',
region: 'us-east-1'
}
}
})
// Should handle permission errors without crashing
await expect(brainy.init()).resolves.not.toThrow()
})
})
describe('S3 Performance Optimizations', () => {
it('should use multipart upload for large objects', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Create large dataset
const largeData = Array(1000).fill(0).map((_, i) => ({
data: `Large item ${i}`,
metadata: {
index: i,
largeField: 'x'.repeat(1000) // Make metadata large
}
}))
// Add large batch
await brainy.addBatch(
largeData.map(d => d.data),
largeData.map(d => d.metadata)
)
// Verify data was uploaded
const putCalls = s3Mock.commandCalls(PutObjectCommand)
expect(putCalls.length).toBeGreaterThan(0)
})
it('should implement caching to reduce S3 calls', async () => {
const testData = {
id: 'cached-item',
vector: [0.1, 0.2, 0.3],
metadata: { cached: true }
}
let getCalls = 0
s3Mock.on(GetObjectCommand).callsFake((input) => {
getCalls++
if (input.Key?.includes('nouns/cached-item')) {
return Promise.resolve({
Body: createMockBody(testData)
})
}
return Promise.reject({ name: 'NoSuchKey' })
})
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
},
cacheConfig: {
hotCacheMaxSize: 100,
warmCacheTTL: 60000
}
}
})
await brainy.init()
// Add item
const id = await brainy.add([0.1, 0.2, 0.3], { cached: true }, { id: 'cached-item' })
// First get - should hit S3
await brainy.get(id)
const firstGetCalls = getCalls
// Second get - should hit cache
await brainy.get(id)
const secondGetCalls = getCalls
// Cache should prevent additional S3 call
expect(secondGetCalls).toBe(firstGetCalls)
})
it('should parallelize S3 operations for better throughput', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add multiple items concurrently
const startTime = Date.now()
const promises = []
for (let i = 0; i < 20; i++) {
promises.push(
brainy.add(`Parallel item ${i}`, { index: i })
)
}
const ids = await Promise.all(promises)
const endTime = Date.now()
expect(ids.length).toBe(20)
// Operations should be parallelized (fast)
// In real scenario, this would be much faster than sequential
expect(endTime - startTime).toBeLessThan(5000) // Should be fast due to mocking
})
})
describe('S3 Data Integrity', () => {
it('should verify data integrity with checksums', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
// Track checksums in PUT operations
const putChecksums: string[] = []
s3Mock.on(PutObjectCommand).callsFake((input) => {
if (input.ChecksumAlgorithm || input.ChecksumCRC32) {
putChecksums.push(input.Key || '')
}
return Promise.resolve({})
})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add data
await brainy.add('Important data', { critical: true })
// Verify checksum was used for critical data
const putCalls = s3Mock.commandCalls(PutObjectCommand)
expect(putCalls.length).toBeGreaterThan(0)
})
it('should handle corrupted data gracefully', async () => {
// Return corrupted JSON
s3Mock.on(GetObjectCommand).resolves({
Body: {
transformToString: async () => '{ corrupt json ][',
transformToByteArray: async () => new TextEncoder().encode('{ corrupt json ][')
}
})
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
// Should handle corrupted data without crashing
await expect(brainy.init()).resolves.not.toThrow()
})
})
describe('S3 Cleanup Operations', () => {
it('should properly clean up S3 objects on delete', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(DeleteObjectCommand).resolves({})
s3Mock.on(DeleteObjectsCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add and then delete item
const id = await brainy.add('To be deleted', { temporary: true })
await brainy.delete(id)
// Verify delete was called
const deleteCalls = s3Mock.commandCalls(DeleteObjectCommand)
expect(deleteCalls.length).toBeGreaterThan(0)
})
it('should batch delete operations for efficiency', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(DeleteObjectsCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Clear all data
await brainy.clear()
// Verify batch delete was used
const batchDeleteCalls = s3Mock.commandCalls(DeleteObjectsCommand)
// Clear operation should use batch delete
expect(batchDeleteCalls.length).toBeGreaterThanOrEqual(0)
})
})
})

View file

@ -0,0 +1,448 @@
/**
* CRITICAL S3 Statistics Tests
*
* These tests ensure that statistics work correctly at scale with S3 storage.
* Statistics are fundamental for monitoring production deployments with millions of records.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mockClient } from 'aws-sdk-client-mock'
import {
S3Client,
GetObjectCommand,
PutObjectCommand,
ListObjectsV2Command,
HeadObjectCommand,
DeleteObjectCommand
} from '@aws-sdk/client-s3'
import { BrainyData } from '../src/index.js'
import { S3CompatibleStorage } from '../src/storage/adapters/s3CompatibleStorage.js'
// Create S3 mock
const s3Mock = mockClient(S3Client)
describe('CRITICAL: S3 Statistics at Scale', () => {
let brainy: BrainyData<any>
let storage: S3CompatibleStorage
beforeEach(() => {
// Reset all mocks before each test
s3Mock.reset()
vi.clearAllTimers()
vi.useFakeTimers()
})
afterEach(async () => {
if (brainy) {
await brainy.clear()
}
vi.useRealTimers()
})
describe('Statistics Persistence and Recovery', () => {
it('should persist statistics to S3 and recover after restart', async () => {
// Setup S3 mock responses
const statisticsData = {
nounCount: { 'service-a': 1000, 'service-b': 500 },
verbCount: { 'service-a': 200, 'service-b': 100 },
metadataCount: { 'service-a': 1000, 'service-b': 500 },
hnswIndexSize: 1500,
lastUpdated: new Date().toISOString()
}
// Mock initial empty state
s3Mock.on(GetObjectCommand).rejectsOnce({ name: 'NoSuchKey' })
// Mock successful save
s3Mock.on(PutObjectCommand).resolves({})
// Initialize Brainy with S3 storage
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add data with different services
await brainy.add('Test data 1', { metadata: 'test1' }, { service: 'service-a' })
await brainy.add('Test data 2', { metadata: 'test2' }, { service: 'service-b' })
// Force statistics flush
await brainy.flushStatistics()
// Verify statistics were saved to S3
const putCalls = s3Mock.commandCalls(PutObjectCommand)
expect(putCalls.length).toBeGreaterThan(0)
// Get current statistics
const stats = await brainy.getStatistics()
expect(stats.nounCount).toBe(2)
expect(stats.serviceBreakdown).toBeDefined()
expect(stats.serviceBreakdown['service-a'].nounCount).toBe(1)
expect(stats.serviceBreakdown['service-b'].nounCount).toBe(1)
// Simulate restart by creating new instance
s3Mock.reset()
// Mock loading saved statistics
s3Mock.on(GetObjectCommand).resolves({
Body: {
transformToString: async () => JSON.stringify({
nounCount: { 'service-a': 1, 'service-b': 1 },
verbCount: { 'service-a': 0, 'service-b': 0 },
metadataCount: { 'service-a': 1, 'service-b': 1 },
hnswIndexSize: 2,
lastUpdated: new Date().toISOString()
})
}
})
const brainy2 = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy2.init()
// Verify statistics were recovered
const recoveredStats = await brainy2.getStatistics()
expect(recoveredStats.nounCount).toBe(2)
expect(recoveredStats.serviceBreakdown['service-a'].nounCount).toBe(1)
expect(recoveredStats.serviceBreakdown['service-b'].nounCount).toBe(1)
})
})
describe('Batching and Throttling', () => {
it('should batch statistics updates to prevent S3 rate limits', async () => {
s3Mock.on(GetObjectCommand).rejectsOnce({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add multiple items rapidly (simulating high load)
const promises = []
for (let i = 0; i < 100; i++) {
promises.push(
brainy.add(`Item ${i}`, { index: i }, { service: `service-${i % 5}` })
)
}
await Promise.all(promises)
// Initially, statistics shouldn't be flushed immediately
const initialPutCalls = s3Mock.commandCalls(PutObjectCommand)
expect(initialPutCalls.length).toBeLessThan(100) // Should batch, not 100 individual calls
// Advance timers to trigger batch flush
vi.advanceTimersByTime(5000)
// Force flush to ensure all statistics are saved
await brainy.flushStatistics()
// Verify statistics are correct
const stats = await brainy.getStatistics()
expect(stats.nounCount).toBe(100)
// Verify batching occurred (much fewer S3 calls than items)
const finalPutCalls = s3Mock.commandCalls(PutObjectCommand)
expect(finalPutCalls.length).toBeLessThan(20) // Should be batched
})
it('should handle S3 throttling (429) gracefully', async () => {
s3Mock.on(GetObjectCommand).rejectsOnce({ name: 'NoSuchKey' })
// Simulate throttling on first attempt, success on retry
let putAttempts = 0
s3Mock.on(PutObjectCommand).callsFake(() => {
putAttempts++
if (putAttempts === 1) {
const error: any = new Error('Too Many Requests')
error.name = 'TooManyRequestsException'
error.$metadata = { httpStatusCode: 429 }
throw error
}
return Promise.resolve({})
})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add data
await brainy.add('Test data', { metadata: 'test' }, { service: 'throttle-test' })
// Force flush - should retry on throttling
await brainy.flushStatistics()
// Verify retry occurred
expect(putAttempts).toBeGreaterThanOrEqual(1)
// Verify statistics were eventually saved
const stats = await brainy.getStatistics()
expect(stats.nounCount).toBe(1)
})
})
describe('Time-Based Partitioning', () => {
it('should partition statistics by date to avoid single-key rate limits', async () => {
s3Mock.on(GetObjectCommand).rejects({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
s3Mock.on(ListObjectsV2Command).resolves({ Contents: [] })
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add data across different time periods
const today = new Date()
await brainy.add('Today item', { date: today }, { service: 'time-test' })
await brainy.flushStatistics()
// Check that statistics are saved with date-based key
const putCalls = s3Mock.commandCalls(PutObjectCommand)
const statisticsCall = putCalls.find(call =>
call.args[0].input.Key?.includes('_system/statistics')
)
expect(statisticsCall).toBeDefined()
// Should include date in the key for partitioning
const key = statisticsCall?.args[0].input.Key
expect(key).toContain(today.toISOString().split('T')[0])
})
})
describe('Backward Compatibility', () => {
it('should read legacy statistics format correctly', async () => {
// Mock legacy statistics format (without service breakdown)
const legacyStats = {
nounCount: 500,
verbCount: 100,
metadataCount: 500,
hnswIndexSize: 500,
lastUpdated: new Date().toISOString()
}
s3Mock.on(GetObjectCommand).resolves({
Body: {
transformToString: async () => JSON.stringify(legacyStats)
}
})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Should handle legacy format gracefully
const stats = await brainy.getStatistics()
expect(stats).toBeDefined()
// Legacy format should be converted to new format with default service
expect(stats.nounCount).toBe(500)
})
it('should migrate legacy statistics to new format on write', async () => {
// Start with legacy format
const legacyStats = {
nounCount: 100,
verbCount: 50,
metadataCount: 100,
hnswIndexSize: 100,
lastUpdated: new Date().toISOString()
}
s3Mock.on(GetObjectCommand).resolvesOnce({
Body: {
transformToString: async () => JSON.stringify(legacyStats)
}
})
s3Mock.on(PutObjectCommand).resolves({})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Add new data
await brainy.add('New data', { metadata: 'new' }, { service: 'migration-test' })
await brainy.flushStatistics()
// Verify new format was saved
const putCalls = s3Mock.commandCalls(PutObjectCommand)
const lastPut = putCalls[putCalls.length - 1]
const savedData = JSON.parse(lastPut.args[0].input.Body as string)
// Should have service-based structure
expect(savedData.nounCount).toBeTypeOf('object')
expect(savedData.nounCount['migration-test']).toBeDefined()
})
})
describe('Concurrent Updates', () => {
it('should handle concurrent statistics updates safely', async () => {
s3Mock.on(GetObjectCommand).rejectsOnce({ name: 'NoSuchKey' })
s3Mock.on(PutObjectCommand).resolves({})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Simulate concurrent additions from multiple services
const services = ['api', 'worker', 'batch', 'stream', 'webhook']
const concurrentOps = []
for (let i = 0; i < 50; i++) {
const service = services[i % services.length]
concurrentOps.push(
brainy.add(`Data ${i}`, { index: i }, { service })
)
}
// Add relationships concurrently
await Promise.all(concurrentOps)
const ids = concurrentOps.map((_, i) => `id-${i}`)
const relationOps = []
for (let i = 0; i < 10; i++) {
relationOps.push(
brainy.relate(
ids[i],
ids[i + 10],
'related',
{ service: services[i % services.length] }
).catch(() => {}) // Ignore if IDs don't exist
)
}
await Promise.all(relationOps)
await brainy.flushStatistics()
// Verify all operations were counted correctly
const stats = await brainy.getStatistics()
expect(stats.nounCount).toBe(50)
// Verify per-service counts
for (const service of services) {
expect(stats.serviceBreakdown[service]).toBeDefined()
expect(stats.serviceBreakdown[service].nounCount).toBe(10) // 50 items / 5 services
}
})
})
describe('Large Scale Statistics', () => {
it('should handle statistics for millions of records efficiently', async () => {
// Mock large existing statistics
const largeStats = {
nounCount: {
'service-1': 1000000,
'service-2': 2000000,
'service-3': 1500000
},
verbCount: {
'service-1': 500000,
'service-2': 750000,
'service-3': 600000
},
metadataCount: {
'service-1': 1000000,
'service-2': 2000000,
'service-3': 1500000
},
hnswIndexSize: 4500000,
lastUpdated: new Date().toISOString()
}
s3Mock.on(GetObjectCommand).resolves({
Body: {
transformToString: async () => JSON.stringify(largeStats)
}
})
s3Mock.on(PutObjectCommand).resolves({})
brainy = new BrainyData({
storage: {
s3Storage: {
bucketName: 'test-bucket',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
region: 'us-east-1'
}
}
})
await brainy.init()
// Get statistics for large dataset
const stats = await brainy.getStatistics()
// Verify large numbers are handled correctly
expect(stats.nounCount).toBe(4500000)
expect(stats.verbCount).toBe(1850000)
// Add more data to large dataset
await brainy.add('New item in large dataset', {}, { service: 'service-1' })
await brainy.flushStatistics()
// Verify increment worked correctly with large numbers
const updatedStats = await brainy.getStatistics()
expect(updatedStats.nounCount).toBe(4500001)
expect(updatedStats.serviceBreakdown['service-1'].nounCount).toBe(1000001)
})
})
})