diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a058ea2b3b..f35b92731b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ on: jobs: test: + name: unit tests runs-on: ubuntu-latest strategy: @@ -148,3 +149,40 @@ jobs: run: | pip install -r ./tests/ci/requirements.txt ./tests/ci/vitest.py + + full-test: + name: full end-to-end test with coverage report + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + matrix: + node-version: [22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Vitest Test + run: | + pip install -r ./tests/ci/requirements.txt + ./tests/ci/full-test.py + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + id: coverage-report + with: + name: (full-test) coverage-report + path: coverage + retention-days: 3 + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: (full-test) server-logs + path: /tmp/backend.log + retention-days: 3 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c5df3843c6..f338ed34fe 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,11 @@ package-lock.json # ====================================================================== tests/client-config.yaml +# ====================================================================== +# test coverage +# ====================================================================== +coverage/ + # ====================================================================== # python # ====================================================================== diff --git a/package-lock.json b/package-lock.json index 6b07030a5d..00a6e5abf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.46.1", + "c8": "^10.1.3", "chalk": "^4.1.0", "clean-css": "^5.3.2", "dotenv": "^16.4.5", @@ -1149,6 +1150,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bufbuild/protobuf": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz", @@ -2813,6 +2824,109 @@ "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -7029,6 +7143,13 @@ "@types/node": "*" } }, + "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/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8714,6 +8835,214 @@ "node": ">= 0.8" } }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/c8/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/c8/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/c8/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -10234,6 +10563,13 @@ "stream-shift": "^1.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -12949,6 +13285,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/javascript-time-ago": { "version": "2.5.12", "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.12.tgz", @@ -15008,6 +15360,13 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -15149,6 +15508,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -17135,6 +17518,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -17147,6 +17546,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -18148,6 +18561,28 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/validator": { "version": "13.15.15", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", @@ -18816,6 +19251,25 @@ "node": ">=6" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", diff --git a/package.json b/package.json index 285ba218e7..f3648ee97f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.46.1", + "c8": "^10.1.3", "chalk": "^4.1.0", "clean-css": "^5.3.2", "dotenv": "^16.4.5", diff --git a/tests/README.md b/tests/README.md index 7f7e7e8a13..bbfc7897bb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,7 +11,7 @@ End-to-end tests for puter-js and http API. -## How to use +## How to use (for Everyone) ### Initialize the Client Config @@ -46,4 +46,56 @@ npx playwright test ```bash npm run test:puterjs-api +``` + +## Coverage (for CI Maintainers) + +```bash +npm install --save-dev c8 +npx c8 -r lcov -r text -o coverage/backend npm start +``` + +## CI Debugging (for CI Maintainers) + +All of the following commands assume a Linux environment. This section is used when: + +- CI is failing. +- CI scripts are updated. + +### API-Test + +Install dependencies: + +```bash +TODO +``` + +Run the CI: + +```bash +./tests/ci/api-test.py +``` + +### Playwright + +Install dependencies: + +```bash +TODO +``` + +Run the CI: + +```bash +./tests/ci/playwright-test.py +``` + +### Vitest + +TODO + +### Full End-to-End Test with Coverage + +```bash +./tests/ci/full-test.py ``` \ No newline at end of file diff --git a/tests/ci/full-test.py b/tests/ci/full-test.py new file mode 100755 index 0000000000..7a782a935a --- /dev/null +++ b/tests/ci/full-test.py @@ -0,0 +1,79 @@ +#! /usr/bin/env python3 + +import os +import signal +import time +import json +import yaml + +import cxc_toolkit + +import common + +PUTER_ROOT = common.PUTER_ROOT + + +def run(): + # ========================================================================= + # clean ports + # ========================================================================= + + # clean port 4100 for backend server + cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True) + + # clean port 50052 for fs-tree-manager server + cxc_toolkit.exec.run_command("fuser -k 50052/tcp", ignore_failure=True) + + # ========================================================================= + # config server + # ========================================================================= + cxc_toolkit.exec.run_command("npm install") + common.init_backend_config() + admin_password = common.get_admin_password() + + # ========================================================================= + # start backend server + # ========================================================================= + cxc_toolkit.exec.run_command("npm install --save-dev c8") + + backend_process = cxc_toolkit.exec.run_background( + "npx c8 --all --include=src/backend --include=extensions --reporter='text' --reporter='html' node ./tools/run-selfhosted.js", + work_dir=PUTER_ROOT, + log_path="/tmp/backend.log", + ) + # wait 10s for the server to start + time.sleep(10) + + # ========================================================================= + # config client + # ========================================================================= + token = common.get_token(admin_password) + common.init_client_config(token) + + # ========================================================================= + # run the test + # ========================================================================= + cxc_toolkit.exec.run_command( + "npx playwright test -g 'stat with uid'", + work_dir=f"{PUTER_ROOT}/tests/playwright", + ) + + import psutil + p = psutil.Process(backend_process.pid) + for child in p.children(recursive=True): + # print(f"terminating child process {child.pid}") + # child.kill() + print(f"sending SIGINT to child process {child.pid}") + os.kill(child.pid, signal.SIGINT) + + # import psutil + # p = psutil.Process(backend_process.pid) + print(f"sending SIGINT to backend process {backend_process.pid}") + os.kill(backend_process.pid, signal.SIGINT) + print(f"waiting for backend process {backend_process.pid} to exit") + backend_process.process.wait() + print(f"backend process {backend_process.pid} exited") + + +if __name__ == "__main__": + run() diff --git a/tests/playwright/package.json b/tests/playwright/package.json index a87f5c818c..0a56ed2a5a 100644 --- a/tests/playwright/package.json +++ b/tests/playwright/package.json @@ -9,7 +9,7 @@ "license": "ISC", "type": "commonjs", "devDependencies": { - "@playwright/test": "^1.56.0", + "@playwright/test": "^1.56.1", "@types/node": "^24.7.2", "yaml": "^2.4.5" } diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index 55a20561bb..8a55f9675a 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -23,12 +23,14 @@ export default defineConfig({ // Disable parallelism since puter fs doesn't provide concurrent safety. workers: 1, + timeout: 60_000, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('')`. */ - // baseURL: 'http://localhost:3000', + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -40,42 +42,5 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, }); diff --git a/tests/playwright/tests/auth/fixtures.ts b/tests/playwright/tests/auth/fixtures.ts new file mode 100644 index 0000000000..4b4bda5190 --- /dev/null +++ b/tests/playwright/tests/auth/fixtures.ts @@ -0,0 +1,130 @@ +import { test as base, expect, Page } from '@playwright/test'; +import { testConfig } from '../../config/test-config'; + +/** + * Helper function to log in a page with the test account. + * This is used internally by fixtures to avoid code duplication. + */ +async function loginPage(page: Page): Promise { + page.on('pageerror', (e) => console.error('[pageerror]', e)); + page.on('console', (m) => console.log('[browser]', m.text())); + + await page.goto(testConfig.frontend_url); + await page.waitForFunction(() => Boolean((window as any).puter), null, { timeout: 10_000 }); + + // Wait until a temporary user is created. + // + // Q: Why we have to wait the completion of this step? + // A: Since this action triggers a `puter.setAuthToken` call and may + // conflict with the user profile used for test. + await expect + .poll(async () => { + return await page.evaluate(async () => { + const puter = (window as any).puter; + return await puter.auth.whoami(); + }); + }, { timeout: 10_000, intervals: [1000] }) + .toBeTruthy(); + + // Wait for the side effects of "temporary user creation", otherwise + // it may overwrite our test logic. + await new Promise(resolve => setTimeout(resolve, 3000)); + + // switch to the test account + await page.evaluate(async ({ api_url, auth_token }) => { + const puter = (window as any).puter; + await puter.setAPIOrigin(api_url); + await puter.setAuthToken(auth_token); + return; + }, { api_url: testConfig.api_url, auth_token: testConfig.auth_token }); + + // verify we are logged in + const whoami = await page.evaluate(async () => { + const puter = (window as any).puter; + return await puter.auth.whoami(); + }); + expect(whoami?.username).toBe(testConfig.username); +} + +/** + * Logged-in Playwright test instance. + * + * This fixture ensures the user is authenticated before each test. + * It extends the base Playwright `test` with a pre-logged-in `page` + * so tests can directly access authenticated routes or APIs. + * + * Example: + * ```ts + * loggedIn('example test', async ({ page }) => { + * const result = await page.evaluate(async () => { + * const puter = (window as any).puter; + * return await puter.auth.whoami(); + * }); + * }); + * ``` + */ +export const loggedIn = base.extend<{ page: Page }>({ + page: async ({ page }, use) => { + await loginPage(page); + + const debug = false; + if (debug) { + // check whoami every 1 second, for 10 seconds + for (let i = 0; i < 10; i++) { + const whoami = await page.evaluate(async () => { + const puter = (window as any).puter; + return await puter.auth.whoami(); + }); + console.log(`checking whoami ${i}, username: ${whoami?.username}`); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + await use(page); + }, +}); + +/** + * Two pages logged-in Playwright test instance. + * + * This fixture ensures both pages are authenticated as the same user before each test. + * It extends the base Playwright `test` with two pre-logged-in pages (`page1` and `page2`) + * so tests can directly access authenticated routes or APIs from both pages. + * + * Example: + * ```ts + * twoPagesLoggedIn('example test', async ({ page1, page2 }) => { + * const result1 = await page1.evaluate(async () => { + * const puter = (window as any).puter; + * return await puter.auth.whoami(); + * }); + * const result2 = await page2.evaluate(async () => { + * const puter = (window as any).puter; + * return await puter.auth.whoami(); + * }); + * // Both result1 and result2 should have the same username + * }); + * ``` + */ +export const twoPagesLoggedIn = base.extend<{ page1: Page; page2: Page }>({ + page1: async ({ browser }, use) => { + const context = await browser.newContext(); + const page = await context.newPage(); + + await loginPage(page); + + await use(page); + + await context.close(); + }, + page2: async ({ browser }, use) => { + const context = await browser.newContext(); + const page = await context.newPage(); + + await loginPage(page); + + await use(page); + + await context.close(); + }, +}); diff --git a/tests/playwright/tests/auth/whoami.spec.ts b/tests/playwright/tests/auth/whoami.spec.ts new file mode 100644 index 0000000000..fc41de9eed --- /dev/null +++ b/tests/playwright/tests/auth/whoami.spec.ts @@ -0,0 +1,12 @@ +import { expect } from '@playwright/test'; +import { testConfig } from '../../config/test-config'; +import { loggedIn } from './fixtures'; + +loggedIn('puter.auth.whoami', async ({ page }) => { + const result = await page.evaluate(async () => { + const puter = (window as any).puter; + return await puter.auth.whoami(); + }); + + expect(result?.username).toBe(testConfig.username); +}); diff --git a/tests/playwright/tests/file-system/change_propagation.spec.ts b/tests/playwright/tests/file-system/change_propagation.spec.ts new file mode 100644 index 0000000000..28a0c38028 --- /dev/null +++ b/tests/playwright/tests/file-system/change_propagation.spec.ts @@ -0,0 +1,61 @@ +import { expect } from '@playwright/test'; +import { validate as isValidUUID } from 'uuid'; +import { FSEntry } from '../../../../src/backend/src/filesystem/definitions/ts/fsentry'; +import { testConfig } from '../../config/test-config'; +import { twoPagesLoggedIn } from '../auth/fixtures'; +import { CHANGE_PROPAGATION_TIME } from './fixtures'; + +// Check the integrity of the FSEntry object. +function checkIntegrity(entry: FSEntry): string | null { + // check essential fields + if (!entry.uid || !isValidUUID(entry.uid)) { + return `Invalid UID: ${entry.uid}`; + } + if (!entry.name || entry.name.trim() === '') { + return `Invalid name: ${entry.name}`; + } + if (!entry.path || entry.path.trim() === '') { + return `Invalid path: ${entry.path}`; + } + if (!entry.parent_id || !isValidUUID(entry.parent_id)) { + return `Invalid parent_id: ${entry.parent_id}`; + } + if (entry.size < 0) { + return `Invalid size: ${entry.size}`; + } + if (typeof entry.is_dir !== 'boolean') { + return `Invalid is_dir type: ${typeof entry.is_dir}`; + } + return null; +} + +twoPagesLoggedIn('change-propagation - mkdir', async ({ page1, page2 }) => { + // Paths + const testPath = `/${testConfig.username}/Desktop`; + const dirName = `_test_dir_${Date.now()}`; + const dirPath = `${testPath}/${dirName}`; + + // --- Session A (page1): perform the action (mkdir) --- + await page1.evaluate(async ({ dirPath }) => { + const puter = (window as any).puter; + await puter.fs.mkdir(dirPath); + }, { dirPath }); + + // Wait for change to be propagated. + await page2.waitForTimeout(CHANGE_PROPAGATION_TIME); + + // --- Session B (page2): observe AFTER mkdir --- + const { entry }: { entry: FSEntry } = await page2.evaluate(async ({ dirPath }) => { + const puter = (window as any).puter; + + const entry = await puter.fs.stat(dirPath); + return { entry }; + }, { dirPath }); + + // Print the complete FSEntry object + console.log('FSEntry object:', JSON.stringify(entry, null, 2)); + + const integrityError = checkIntegrity(entry); + expect(integrityError).toBeNull(); +}); + diff --git a/tests/playwright/tests/file-system/copy_cart.spec.ts b/tests/playwright/tests/file-system/copy_cart.spec.ts index ac7d4dbb62..533dac7b2f 100644 --- a/tests/playwright/tests/file-system/copy_cart.spec.ts +++ b/tests/playwright/tests/file-system/copy_cart.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('copy file with path format', async ({ page }) => { +testDirCleaned('copy file with path format', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_1`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -33,7 +33,7 @@ test('copy file with path format', async ({ page }) => { expect(result[0].copied.name).toBe('a_file.txt'); }); -test('copy file with specified name', async ({ page }) => { +testDirCleaned('copy file with specified name', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_2`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -64,7 +64,7 @@ test('copy file with specified name', async ({ page }) => { expect(result[0].copied.name).toBe(newName); }); -test('copy file with overwrite', async ({ page }) => { +testDirCleaned('copy file with overwrite', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_3`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -95,7 +95,7 @@ test('copy file with overwrite', async ({ page }) => { expect(result[0]).toBeTruthy(); }); -test('copy file without overwrite to directory with existing file should error', async ({ page }) => { +testDirCleaned('copy file without overwrite to directory with existing file should error', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_4`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -125,7 +125,7 @@ test('copy file without overwrite to directory with existing file should error', expect(result.code).toBeTruthy(); }); -test('copy file to file destination should error', async ({ page }) => { +testDirCleaned('copy file to file destination should error', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_6`; const sourceFile = `${testPath}/a/a_file.txt`; const destFile = `${testPath}/b`; @@ -154,7 +154,7 @@ test('copy file to file destination should error', async ({ page }) => { expect(result.code).toBe('dest_is_not_a_directory'); }); -test('copy empty directory', async ({ page }) => { +testDirCleaned('copy empty directory', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_7`; const sourceDir = `${testPath}/a/a_directory`; const destDir = `${testPath}/b`; @@ -184,7 +184,7 @@ test('copy empty directory', async ({ page }) => { expect(result[0].copied.name).toBe('a_directory'); }); -test('copy full directory', async ({ page }) => { +testDirCleaned('copy full directory', async ({ page }) => { const testPath = `${BASE_PATH}/copy_cart_8`; const sourceDir = `${testPath}/a/a_directory`; const destDir = `${testPath}/b`; diff --git a/tests/playwright/tests/file-system/delete.spec.ts b/tests/playwright/tests/file-system/delete.spec.ts index b536150a39..bb004a9b18 100644 --- a/tests/playwright/tests/file-system/delete.spec.ts +++ b/tests/playwright/tests/file-system/delete.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('delete for normal file', async ({ page }) => { +testDirCleaned('delete for normal file', async ({ page }) => { const testPath = `${BASE_PATH}/delete_test_1`; const testFile = `${testPath}/test_delete.txt`; @@ -30,7 +30,7 @@ test('delete for normal file', async ({ page }) => { expect(result.exists).toBe(false); }); -test('error for non-existing file', async ({ page }) => { +testDirCleaned('error for non-existing file', async ({ page }) => { const testPath = `${BASE_PATH}/delete_test_2`; const testFile = `${testPath}/test_delete.txt`; @@ -53,7 +53,7 @@ test('error for non-existing file', async ({ page }) => { expect(result.success).toBe(false); }); -test('delete for directory', async ({ page }) => { +testDirCleaned('delete for directory', async ({ page }) => { const testPath = `${BASE_PATH}/delete_test_3`; const testDir = `${testPath}/test_delete_dir`; @@ -81,7 +81,7 @@ test('delete for directory', async ({ page }) => { expect(result.exists).toBe(false); }); -test('delete for non-empty directory with recursive=true', async ({ page }) => { +testDirCleaned('delete for non-empty directory with recursive=true', async ({ page }) => { const testPath = `${BASE_PATH}/delete_test_5`; const testDir = `${testPath}/test_delete_dir`; const testFile = `${testDir}/test.txt`; diff --git a/tests/playwright/tests/file-system/fixtures.ts b/tests/playwright/tests/file-system/fixtures.ts index a621fd98ce..8f9ff69e32 100644 --- a/tests/playwright/tests/file-system/fixtures.ts +++ b/tests/playwright/tests/file-system/fixtures.ts @@ -1,7 +1,5 @@ -import { test as base, expect, Page } from '@playwright/test'; -import { validate as isValidUUID } from 'uuid'; -import { FSEntry } from '../../../../src/backend/src/filesystem/definitions/ts/fsentry'; -import { testConfig } from '../../config/test-config'; +import { Page } from '@playwright/test'; +import { loggedIn } from '../auth/fixtures'; // The maximum time needed for file-system change to be propagated from // one session to others. @@ -16,25 +14,41 @@ export const ERROR_CODES = [ 'source_does_not_exist', ]; -export const test = base.extend<{ page: Page }>({ - page: async ({ browser }, use) => { - const ctx = await browser.newContext(); - const page = await ctx.newPage(); - await bootstrap(page); - +/** + * A Playwright test fixture that ensures a clean test directory on the server. + * + * This fixture extends the {@link loggedIn} test, guaranteeing the user is already authenticated. + * Before each test, it creates (or resets) the `/admin/tests` directory on the backend, + * ensuring it exists and is completely empty. + * + * Use this when your test logic depends on a known-clean workspace for file-system operations. + * + * Example: + * ```ts + * testDirCleaned('demo test', async ({ page }) => { + * await page.evaluate(async () => { + * const puter = (window as any).puter; + * const result = await puter.fs.stat('/admin/tests'); + * console.log('result:', result); + * }); + * }); + * ``` + */ +export const testDirCleaned = loggedIn.extend<{ page: Page }>({ + page: async ({ page }, use) => { await page.evaluate(async ({ BASE_PATH }) => { const puter = (window as any).puter; try { await puter.fs.delete(BASE_PATH, { recursive: true }); - } catch( error ) { + } catch (error) { // ignore error console.error('delete error:', error); } try { await puter.fs.mkdir(BASE_PATH); - } catch( error ) { + } catch (error) { console.error('mkdir error:', error); throw error; } @@ -44,80 +58,3 @@ export const test = base.extend<{ page: Page }>({ }, }); -// Check the integrity of the FSEntry object. -function checkIntegrity(entry: FSEntry): string | null { - // check essential fields - if ( !entry.uid || !isValidUUID(entry.uid) ) { - return `Invalid UID: ${entry.uid}`; - } - if ( !entry.name || entry.name.trim() === '' ) { - return `Invalid name: ${entry.name}`; - } - if ( !entry.path || entry.path.trim() === '' ) { - return `Invalid path: ${entry.path}`; - } - if ( !entry.parent_id || !isValidUUID(entry.parent_id) ) { - return `Invalid parent_id: ${entry.parent_id}`; - } - if ( entry.size < 0 ) { - return `Invalid size: ${entry.size}`; - } - if ( typeof entry.is_dir !== 'boolean' ) { - return `Invalid is_dir type: ${typeof entry.is_dir}`; - } - return null; -} - -async function bootstrap(page: Page) { - page.on('pageerror', (e) => console.error('[pageerror]', e)); - page.on('console', (m) => console.log('[browser]', m.text())); - - await page.goto(testConfig.frontend_url); // establish origin - await page.addScriptTag({ url: '/puter.js/v2' }); // load bundle - await page.waitForFunction(() => Boolean((window as any).puter), null, { timeout: 10_000 }); - - await page.evaluate(async ({ api_url, auth_token }) => { - const puter = (window as any).puter; - await puter.setAPIOrigin(api_url); - await puter.setAuthToken(auth_token); - return; - }, { api_url: testConfig.api_url, auth_token: testConfig.auth_token }); -} - -base('change-propagation - mkdir', async ({ browser }) => { - const ctxA = await browser.newContext(); - const ctxB = await browser.newContext(); - const pageA = await ctxA.newPage(); - const pageB = await ctxB.newPage(); - await Promise.all([bootstrap(pageA), bootstrap(pageB)]); - - // Paths - const testPath = `/${testConfig.username}/Desktop`; - const dirName = `_test_dir_${Date.now()}`; - const dirPath = `${testPath}/${dirName}`; - - // --- Session A: perform the action (mkdir) --- - await pageA.evaluate(async ({ dirPath }) => { - const puter = (window as any).puter; - await puter.fs.mkdir(dirPath); - }, { dirPath }); - - // Wait for change to be propagated. - await pageB.waitForTimeout(CHANGE_PROPAGATION_TIME); - - // --- Session B: observe AFTER mkdir --- - const { entry }: { entry: FSEntry } = await pageB.evaluate(async ({ dirPath }) => { - const puter = (window as any).puter; - - const entry = await puter.fs.stat(dirPath); - return { entry }; - }, { dirPath }); - - // Print the complete FSEntry object - console.log('FSEntry object:', JSON.stringify(entry, null, 2)); - - const integrityError = checkIntegrity(entry); - expect(integrityError).toBeNull(); - - await Promise.all([ctxA.close(), ctxB.close()]); -}); diff --git a/tests/playwright/tests/file-system/mkdir.spec.ts b/tests/playwright/tests/file-system/mkdir.spec.ts index d7a50a1391..64cc622800 100644 --- a/tests/playwright/tests/file-system/mkdir.spec.ts +++ b/tests/playwright/tests/file-system/mkdir.spec.ts @@ -1,10 +1,10 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, ERROR_CODES, test } from './fixtures'; +import { BASE_PATH, ERROR_CODES, testDirCleaned } from './fixtures'; // NB: Don't test "parent + path" api for puter-js, it's only supported on http // api: https://github.com/HeyPuter/puter/blob/9bdb139f7a82ef610e6beb76b91014ac530828a4/src/puter-js/src/modules/FileSystem/operations/mkdir.js#L48-L49 -test('recursive mkdir', async ({ page }) => { +testDirCleaned('recursive mkdir', async ({ page }) => { // Test recursive mkdir with create_missing_parents const path = `${BASE_PATH}/a/b/c/d/e/f/g`; const result = await page.evaluate(async ({ path }) => { @@ -24,7 +24,7 @@ test('recursive mkdir', async ({ page }) => { console.log('result?', result); }); -test('mkdir dedupe name', async ({ page }) => { +testDirCleaned('mkdir dedupe name', async ({ page }) => { const basePath = `${BASE_PATH}/dedupe_test`; // Create initial directory @@ -73,7 +73,7 @@ test('mkdir dedupe name', async ({ page }) => { } }); -test('mkdir in root directory is prohibited', async ({ page }) => { +testDirCleaned('mkdir in root directory is prohibited', async ({ page }) => { // Test full path format let error_code = await page.evaluate(async () => { const puter = (window as any).puter; @@ -99,7 +99,7 @@ test('mkdir in root directory is prohibited', async ({ page }) => { expect(ERROR_CODES.includes(error_code)).toBe(true); }); -test('full path api with create_missing_parents', async ({ page }) => { +testDirCleaned('full path api with create_missing_parents', async ({ page }) => { const testPath = `${BASE_PATH}/full_path_api/create_missing_parents_works`; const targetPath = `${testPath}/a/b/c`; diff --git a/tests/playwright/tests/file-system/move.spec.ts b/tests/playwright/tests/file-system/move.spec.ts index 9734e09551..9951186e30 100644 --- a/tests/playwright/tests/file-system/move.spec.ts +++ b/tests/playwright/tests/file-system/move.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, ERROR_CODES, test } from './fixtures'; +import { BASE_PATH, ERROR_CODES, testDirCleaned } from './fixtures'; -test('move file', async ({ page }) => { +testDirCleaned('move file', async ({ page }) => { const sourceFile = `${BASE_PATH}/just_a_file.txt`; const targetFile = `${BASE_PATH}/just_a_file_moved.txt`; @@ -58,7 +58,7 @@ test('move file', async ({ page }) => { expect(ERROR_CODES.includes(sourceError)).toBe(true); }); -test('move file to existing file', async ({ page }) => { +testDirCleaned('move file to existing file', async ({ page }) => { const sourceFile = `${BASE_PATH}/just_a_file.txt`; const targetFile = `${BASE_PATH}/dir_with_contents/a.txt`; @@ -88,7 +88,7 @@ test('move file to existing file', async ({ page }) => { expect(ERROR_CODES.includes(errorCode), `unexpected error code: ${errorCode}`).toBe(true); }); -test('move directory', async ({ page }) => { +testDirCleaned('move directory', async ({ page }) => { const sourceDir = `${BASE_PATH}/dir_no_contents`; const targetDir = `${BASE_PATH}/dir_no_contents_moved`; @@ -145,7 +145,7 @@ test('move directory', async ({ page }) => { expect(ERROR_CODES.includes(sourceError)).toBe(true); }); -test('move file and create parents', async ({ page }) => { +testDirCleaned('move file and create parents', async ({ page }) => { const sourceFile = `${BASE_PATH}/just_a_file.txt`; const targetFile = `${BASE_PATH}/dir_with_contents/q/w/e/just_a_file.txt`; diff --git a/tests/playwright/tests/file-system/move_cart.spec.ts b/tests/playwright/tests/file-system/move_cart.spec.ts index 2ec62c41e4..d06b3eb361 100644 --- a/tests/playwright/tests/file-system/move_cart.spec.ts +++ b/tests/playwright/tests/file-system/move_cart.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('move file with path format', async ({ page }) => { +testDirCleaned('move file with path format', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_1`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -31,7 +31,7 @@ test('move file with path format', async ({ page }) => { expect(result.moved.name).toBe('a_file.txt'); }); -test('move file with specified name', async ({ page }) => { +testDirCleaned('move file with specified name', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_2`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -62,7 +62,7 @@ test('move file with specified name', async ({ page }) => { expect(result.moved.name).toBe(newName); }); -test('move file with overwrite to directory', async ({ page }) => { +testDirCleaned('move file with overwrite to directory', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_3`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -92,7 +92,7 @@ test('move file with overwrite to directory', async ({ page }) => { expect(result).toBeTruthy(); }); -test('move file without overwrite to directory with existing file should error', async ({ page }) => { +testDirCleaned('move file without overwrite to directory with existing file should error', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_4`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; @@ -122,7 +122,7 @@ test('move file without overwrite to directory with existing file should error', expect(result.code).toBeTruthy(); }); -test('move file to file destination should error', async ({ page }) => { +testDirCleaned('move file to file destination should error', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_6`; const sourceFile = `${testPath}/a/a_file.txt`; const destFile = `${testPath}/b`; @@ -151,7 +151,7 @@ test('move file to file destination should error', async ({ page }) => { expect(result.code).toBe('dest_is_not_a_directory'); }); -test('move file with uid format', async ({ page }) => { +testDirCleaned('move file with uid format', async ({ page }) => { const testPath = `${BASE_PATH}/move_cart_7`; const sourceFile = `${testPath}/a/a_file.txt`; const destDir = `${testPath}/b`; diff --git a/tests/playwright/tests/file-system/readdir.spec.ts b/tests/playwright/tests/file-system/readdir.spec.ts index d27f041e95..9db20b5bb9 100644 --- a/tests/playwright/tests/file-system/readdir.spec.ts +++ b/tests/playwright/tests/file-system/readdir.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('readdir test', async ({ page }) => { +testDirCleaned('readdir test', async ({ page }) => { // Create test directory const testDir = `${BASE_PATH}/test_readdir`; @@ -81,7 +81,7 @@ test('readdir test', async ({ page }) => { } }); -test('readdir of root shouldn\'t return everything', async ({ page }) => { +testDirCleaned('readdir of root shouldn\'t return everything', async ({ page }) => { const result = await page.evaluate(async () => { const puter = (window as any).puter; try { diff --git a/tests/playwright/tests/file-system/stat.spec.ts b/tests/playwright/tests/file-system/stat.spec.ts index b3a19315a3..b902630800 100644 --- a/tests/playwright/tests/file-system/stat.spec.ts +++ b/tests/playwright/tests/file-system/stat.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('stat with path (no flags)', async ({ page }) => { +testDirCleaned('stat with path (no flags)', async ({ page }) => { const TEST_FILENAME = 'test_stat.txt'; const testPath = `${BASE_PATH}/${TEST_FILENAME}`; @@ -34,7 +34,7 @@ test('stat with path (no flags)', async ({ page }) => { expect(result.uid).toBeDefined(); }); -test('stat with uid', async ({ page }) => { +testDirCleaned('stat with uid', async ({ page }) => { const TEST_FILENAME = 'test_stat.txt'; const testPath = `${BASE_PATH}/${TEST_FILENAME}`; @@ -80,7 +80,7 @@ test('stat with uid', async ({ page }) => { expect(result.uid).toBe(uid); }); -test('stat with no path or uid provided fails', async ({ page }) => { +testDirCleaned('stat with no path or uid provided fails', async ({ page }) => { const result = await page.evaluate(async () => { const puter = (window as any).puter; try { @@ -94,7 +94,7 @@ test('stat with no path or uid provided fails', async ({ page }) => { expect(result.success).toBe(false); }); -test('stat with versions', async ({ page }) => { +testDirCleaned('stat with versions', async ({ page }) => { const TEST_FILENAME = 'test_stat.txt'; const testPath = `${BASE_PATH}/${TEST_FILENAME}`; @@ -131,7 +131,7 @@ test('stat with versions', async ({ page }) => { expect(Array.isArray(result.versions)).toBe(true); }); -test('stat with shares', async ({ page }) => { +testDirCleaned('stat with shares', async ({ page }) => { const TEST_FILENAME = 'test_stat.txt'; const testPath = `${BASE_PATH}/${TEST_FILENAME}`; @@ -169,7 +169,7 @@ test('stat with shares', async ({ page }) => { expect(Array.isArray(result.shares.apps)).toBe(true); }); -test('stat with subdomains', async ({ page }) => { +testDirCleaned('stat with subdomains', async ({ page }) => { const dirName = 'test_stat_subdomains'; const testPath = `${BASE_PATH}/${dirName}`; @@ -205,7 +205,7 @@ test('stat with subdomains', async ({ page }) => { console.log('RESULT', result); }); -test('stat with size', async ({ page }) => { +testDirCleaned('stat with size', async ({ page }) => { const TEST_FILENAME = 'test_stat.txt'; const testPath = `${BASE_PATH}/${TEST_FILENAME}`; diff --git a/tests/playwright/tests/file-system/write_and_read.spec.ts b/tests/playwright/tests/file-system/write_and_read.spec.ts index f56aed9ded..6dcc2fd5c4 100644 --- a/tests/playwright/tests/file-system/write_and_read.spec.ts +++ b/tests/playwright/tests/file-system/write_and_read.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('read matches what was written', async ({ page }) => { +testDirCleaned('read matches what was written', async ({ page }) => { const fileName = 'test_rw.txt'; const testPath = `${BASE_PATH}/${fileName}`; @@ -21,7 +21,7 @@ test('read matches what was written', async ({ page }) => { expect(result).toBe('example\n'); }); -test('write without overwrite creates deduped name', async ({ page }) => { +testDirCleaned('write without overwrite creates deduped name', async ({ page }) => { const fileName = 'test_rw.txt'; const testPath = `${BASE_PATH}/${fileName}`; @@ -47,7 +47,7 @@ test('write without overwrite creates deduped name', async ({ page }) => { expect(result).toBeTruthy(); }); -test('write with overwrite updates file', async ({ page }) => { +testDirCleaned('write with overwrite updates file', async ({ page }) => { const fileName = 'test_rw.txt'; const testPath = `${BASE_PATH}/${fileName}`; @@ -73,7 +73,7 @@ test('write with overwrite updates file', async ({ page }) => { expect(result).toBe('yes-change\n'); }); -test('read with version id', async ({ page }) => { +testDirCleaned('read with version id', async ({ page }) => { const fileName = 'test_rw.txt'; const testPath = `${BASE_PATH}/${fileName}`; @@ -98,7 +98,7 @@ test('read with version id', async ({ page }) => { expect(result.success).toBe(true); }); -test('read with no path or uid provided fails', async ({ page }) => { +testDirCleaned('read with no path or uid provided fails', async ({ page }) => { const result = await page.evaluate(async () => { const puter = (window as any).puter; try { @@ -113,7 +113,7 @@ test('read with no path or uid provided fails', async ({ page }) => { expect(result.code).toBeTruthy(); }); -test('read non-existing file fails', async ({ page }) => { +testDirCleaned('read non-existing file fails', async ({ page }) => { const result = await page.evaluate(async ({ basePath }) => { const puter = (window as any).puter; try { diff --git a/tests/playwright/tests/file-system/write_cart.spec.ts b/tests/playwright/tests/file-system/write_cart.spec.ts index 475e550390..75250acc19 100644 --- a/tests/playwright/tests/file-system/write_cart.spec.ts +++ b/tests/playwright/tests/file-system/write_cart.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { BASE_PATH, test } from './fixtures'; +import { BASE_PATH, testDirCleaned } from './fixtures'; -test('write to new directory with default name', async ({ page }) => { +testDirCleaned('write to new directory with default name', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_1`; // Create directory @@ -24,7 +24,7 @@ test('write to new directory with default name', async ({ page }) => { expect(result.name).toBe('uploaded_name.txt'); }); -test('write with specified name', async ({ page }) => { +testDirCleaned('write with specified name', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_2`; // Create directory @@ -46,7 +46,7 @@ test('write with specified name', async ({ page }) => { expect(result).toBeTruthy(); }); -test('write with overwrite option', async ({ page }) => { +testDirCleaned('write with overwrite option', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_3`; const fileName = 'test_overwrite.txt'; @@ -85,7 +85,7 @@ test('write with overwrite option', async ({ page }) => { expect(readResult).toBe('updated content\n'); }); -test('write to directory using UID', async ({ page }) => { +testDirCleaned('write to directory using UID', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_4`; // Create directory and get UID @@ -110,7 +110,7 @@ test('write to directory using UID', async ({ page }) => { expect(result.uid).toBeTruthy(); }); -test('write with dedupe name option', async ({ page }) => { +testDirCleaned('write with dedupe name option', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_5`; const fileName = 'dedupe_test.txt'; @@ -149,7 +149,7 @@ test('write with dedupe name option', async ({ page }) => { expect(result.result.name).toMatch(/dedupe_test \(\d\)\.txt/); }); -test('write string data', async ({ page }) => { +testDirCleaned('write string data', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_6`; // Create directory @@ -178,7 +178,7 @@ test('write string data', async ({ page }) => { expect(readResult).toBe('Hello World\n'); }); -test('write to file instead of directory should error', async ({ page }) => { +testDirCleaned('write to file instead of directory should error', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_7`; const fileName = 'destination.txt'; @@ -214,7 +214,7 @@ test('write to file instead of directory should error', async ({ page }) => { expect(result.success !== undefined).toBe(true); }); -test('write without overwrite on existing file should error', async ({ page }) => { +testDirCleaned('write without overwrite on existing file should error', async ({ page }) => { const testPath = `${BASE_PATH}/write_test_8`; const fileName = 'existing.txt'; const dedupedFileName = 'existing (1).txt'; diff --git a/tests/playwright/tests/whoami.spec.ts b/tests/playwright/tests/whoami.spec.ts deleted file mode 100644 index 492dab902a..0000000000 --- a/tests/playwright/tests/whoami.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { testConfig } from '../config/test-config'; - -test('puter.auth.whoami', async ({ page }) => { - if ( !testConfig.auth_token ) { - throw new Error('authToken is required in client-config.yaml'); - } - - page.on('pageerror', (err) => console.error('[pageerror]', err)); - page.on('console', (msg) => console.log('[browser]', msg.text())); - - // 1) Open any page served by your backend to establish same-origin - await page.goto(testConfig.frontend_url); // even a 404 page is fine; origin is set - - // 2) Load the real bundle from the same origin - await page.addScriptTag({ url: '/puter.js/v2' }); - - // 3) Wait for global - await page.waitForFunction(() => Boolean((window as any).puter), null, { timeout: 10000 }); - - // 4) Call whoami in the browser context - const result = await page.evaluate(async (testConfig) => { - const puter = (window as any).puter; - - await puter.setAPIOrigin(testConfig.api_url); - await puter.setAuthToken(testConfig.auth_token); - - return await puter.auth.whoami(); - }, testConfig); - - expect(result?.username).toBe(testConfig.username); - - const result2 = await page.evaluate(async () => { - const puter = (window as any).puter; - return await puter.auth.whoami(); - }); - - expect(result2?.username).toBe(testConfig.username); -}); - -test('connect to prod puter', async ({ page }) => { - page.on('pageerror', (err) => console.error('[pageerror]', err)); - page.on('console', (msg) => console.log('[browser]', msg.text())); - - const prodURL = 'https://puter.com'; - - // Go to production URL - await page.goto(prodURL); - - // Wait for 5 seconds then exit - await page.waitForTimeout(5000); -}); diff --git a/tools/run-selfhosted.js b/tools/run-selfhosted.js index 17b97c78ba..7e37d73361 100644 --- a/tools/run-selfhosted.js +++ b/tools/run-selfhosted.js @@ -38,8 +38,8 @@ const surrounding_box = (col, lines) => { const max_length = Math.max(...lengths); const c = str => `\x1b[${col}m${str}\x1b[0m`; const bar = c(Array(max_length + 4).fill('━').join('')); - for ( let i = 0 ; i < lines.length ; i++ ) { - while ( lines[i].length < max_length ) { + for (let i = 0; i < lines.length; i++) { + while (lines[i].length < max_length) { lines[i] += ' '; } lines[i] = `${c('┃ ')} ${lines[i]} ${c(' ┃')}`; @@ -60,13 +60,13 @@ const surrounding_box = (col, lines) => { // ACTUAL VERSION CHECK const [major, minor] = process.versions.node.split('.').map(Number); - if ( major < lowest_allowed ) { + if (major < lowest_allowed) { const lines = []; lines.push(`Please use a version of Node.js ${lowest_allowed} or newer.`); lines.push(`Issues with node ${process.versions.node}:`); // We also show the user the reasons in case they want to know - for ( const { under, reasons } of ver_info ) { - if ( major < under ) { + for (const { under, reasons } of ver_info) { + if (major < under) { lines.push(` - ${reasons.join(', ')}`); } } @@ -77,7 +77,7 @@ const surrounding_box = (col, lines) => { } // Annoying polyfill for inconsistency in different node versions -if ( ! import.meta.filename ) { +if (! import.meta.filename) { Object.defineProperty(import.meta, 'filename', { get: () => import.meta.url.slice('file://'.length), }) @@ -104,7 +104,7 @@ const main = async () => { const k = new Kernel({ entry_path: import.meta.filename }); - for ( const mod of EssentialModules ) { + for (const mod of EssentialModules) { k.add_module(new mod()); } k.add_module(new DatabaseModule()); @@ -117,10 +117,10 @@ const main = async () => { k.add_module(new PuterAIModule()); k.add_module(new InternetModule()); k.add_module(new DNSModule()); - if ( process.env.PERFMON ) { + if (process.env.PERFMON) { k.add_module(new PerfMonModule()); } - if ( process.env.UNSAFE_PUTER_DEV ) { + if (process.env.UNSAFE_PUTER_DEV) { k.add_module(new DevelopmentModule()); } k.boot(); @@ -161,8 +161,8 @@ const early_init_errors = [ // null coalescing operator const nco = (...args) => { - for ( const arg of args ) { - if ( arg !== undefined && arg !== null ) { + for (const arg of args) { + if (arg !== undefined && arg !== null) { return arg; } } @@ -172,18 +172,18 @@ const nco = (...args) => { const _print_error_help = (error_help) => { const lines = []; lines.push(nco(error_help.title, error_help.text)); - for ( const note of (nco(error_help.notes, [])) ) { + for (const note of (nco(error_help.notes, []))) { lines.push(`📝 ${note}`) } - if ( error_help.suggestions ) { + if (error_help.suggestions) { lines.push('Suggestions:'); - for ( const suggestion of error_help.suggestions ) { + for (const suggestion of error_help.suggestions) { lines.push(`- ${suggestion}`); } } - if ( error_help.technical_notes ) { + if (error_help.technical_notes) { lines.push('Technical Notes:'); - for ( const note of error_help.technical_notes ) { + for (const note of error_help.technical_notes) { lines.push(`- ${note}`); } } @@ -195,9 +195,9 @@ const _print_error_help = (error_help) => { try { await main(); } catch (e) { - for ( const error_help of early_init_errors ) { + for (const error_help of early_init_errors) { const message = e && e.message; - if ( e.message && e.message.includes(error_help.text) ) { + if (e.message && e.message.includes(error_help.text)) { _print_error_help(error_help); break; } @@ -205,3 +205,17 @@ const _print_error_help = (error_help) => { throw e; } })(); + +// // quit after 10 seconds +// setTimeout(() => { +// process.exit(0); +// }, 10000); + +// Graceful shutdown for c8 to flush coverage +function shutdown() { + console.log('Shutting down...'); + process.exit(0); // triggers c8 to write coverage +} + +process.on('SIGINT', shutdown); // Ctrl-C +process.on('SIGTERM', shutdown); // kill diff --git a/tsconfig.json b/tsconfig.json index c7c572d645..926ff25812 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "**/test/**", "**/tests/**", "node_modules", - "dist" + "dist", + "volatile" ] } \ No newline at end of file