This commit is contained in:
Nariman Jelveh
2025-10-28 17:09:40 -07:00
86 changed files with 3367 additions and 3535 deletions
+108 -17
View File
@@ -2,9 +2,9 @@ name: test
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
jobs:
test:
@@ -15,21 +15,44 @@ jobs:
node-version: [20.x, 22.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
rm package-lock.json
npm install -g npm@latest
npm install
npm run test
- name: Build
run: |
rm package-lock.json
npm install -g npm@latest
npm install
npm run test
api-test:
name: backend (node env, api-test)
runs-on: ubuntu-latest
timeout-minutes: 5
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: API Test
run: |
pip install -r ./tests/ci/requirements.txt
./tests/ci/api-test.py
playwright-test:
name: puterjs (browser env, playwright)
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -44,8 +67,76 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: API Test
- name: Install Dependencies
run: npm install
working-directory: ./tests/playwright
- name: Install Playwright Browsers
run: npx playwright install --with-deps
working-directory: ./tests/playwright
- name: Playwright Test
run: |
pip install -r ./tools/api-tester/ci/requirements.txt
./tools/api-tester/ci/run.py
pip install -r ./tests/ci/requirements.txt
./tests/ci/playwright-test.py
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: server-logs
path: |
/tmp/backend.log
/tmp/fs-tree-manager.log
retention-days: 3
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: config-files
path: |
./volatile/config/config.json
./src/fs_tree_manager/config.yaml
./tests/client-config.yaml
retention-days: 3
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
id: playwright-report
with:
name: playwright-report
path: tests/playwright/playwright-report/
retention-days: 3
- name: Get Playwright artifact URL
run: |
ARTIFACT_URL=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts \
--jq '.artifacts[] | select(.name=="playwright-report") | .archive_download_url')
echo "url=$ARTIFACT_URL" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Output artifact URL
run: echo 'Artifact URL is ${{ steps.playwright-report.outputs.artifact-url }}'
vitest:
name: puterjs (node env, vitest)
runs-on: ubuntu-latest
timeout-minutes: 5
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/vitest.py
+13 -1
View File
@@ -45,10 +45,22 @@ jsconfig.json
# the exact tree installed in the node_modules folder
package-lock.json
# ======================================================================
# playwright test (currently only test the file-system)
# ======================================================================
tests/client-config.yaml
# ======================================================================
# python
# ======================================================================
__pycache__/
# ======================================================================
# other
# ======================================================================
# AI STUFF
AGENTS.md
.roo
# source maps
*.map
+54
View File
@@ -0,0 +1,54 @@
## Summary
Playwright test the puter-js API in browser environment.
## Motivation
Some features of the puter-js/puter-GUI only work in the browser environment:
- file system
- naive-cache
- client-replica (WIP)
- wspush
## Setup
Install dependencies:
```sh
cd ./tests/playwright
npm install
npx playwright install --with-deps
```
Initialize the client config (working directory: `./tests/playwright`):
1. `cp ../example-client-config.yaml ../client-config.yaml`
2. Edit the `client-config.yaml` to set the `auth_token`
## Run tests
### CLI
Working directory: `./tests/playwright`
```sh
# run all tests
npx playwright test
# run a test by name
# e.g: npx playwright test -g "mkdir in root directory is prohibited"
npx playwright test -g "mkdir in root directory is prohibited"
# run the tests that failed in the last test run
npx playwright test --last-failed
# open the report of the last test run in the browser
npx playwright show-report
```
### VSCode/Cursor
1. Install the "Playwright Test for VSCode" extension.
2. Go to "Testing" tab in the sidebar.
3. Click buttons to run tests.
+23
View File
@@ -51,6 +51,7 @@ export default defineConfig([
// TypeScript support block
{
files: ['**/*.ts'],
ignores: ['tests/**/*.ts'],
languageOptions: {
parser: tseslintParser,
parserOptions: {
@@ -70,6 +71,28 @@ export default defineConfig([
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
},
},
// TypeScript support for tests
{
files: ['tests/**/*.ts'],
languageOptions: {
parser: tseslintParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tests/tsconfig.json',
},
},
plugins: {
'@typescript-eslint': tseslintPlugin,
},
rules: {
// Recommended rules for TypeScript
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
},
},
{
plugins: {
js,
+118 -1
View File
@@ -36,6 +36,7 @@
"devDependencies": {
"@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"chalk": "^4.1.0",
@@ -50,6 +51,7 @@
"license-check-and-add": "^4.0.5",
"mocha": "^10.6.0",
"nodemon": "^3.1.0",
"ts-proto": "^2.8.0",
"typescript": "^5.4.5",
"uglify-js": "^3.17.4",
"vite-plugin-static-copy": "^3.1.3",
@@ -891,6 +893,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -1146,6 +1149,13 @@
"node": ">=6.9.0"
}
},
"node_modules/@bufbuild/protobuf": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.0.tgz",
"integrity": "sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==",
"dev": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@canvas/image-data": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz",
@@ -3032,6 +3042,7 @@
"resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.12.tgz",
"integrity": "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "^0.22.12"
}
@@ -3068,6 +3079,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz",
"integrity": "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3080,6 +3092,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz",
"integrity": "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3104,6 +3117,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.12.tgz",
"integrity": "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12",
"tinycolor2": "^1.6.0"
@@ -3147,6 +3161,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz",
"integrity": "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3270,6 +3285,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz",
"integrity": "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3282,6 +3298,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz",
"integrity": "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3297,6 +3314,7 @@
"resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz",
"integrity": "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/utils": "^0.22.12"
},
@@ -3604,6 +3622,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz",
"integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -3613,6 +3632,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.49.1.tgz",
"integrity": "sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api": "^1.0.0"
},
@@ -7266,6 +7286,13 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
@@ -7312,6 +7339,7 @@
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
@@ -7854,7 +7882,8 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
@@ -7904,6 +7933,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8584,6 +8614,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -8802,6 +8833,19 @@
"randomstring": "^1.3.0"
}
},
"node_modules/case-anything": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/centra": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz",
@@ -8816,6 +8860,7 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"license": "MIT",
"peer": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
@@ -10143,6 +10188,29 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dprint-node": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3"
}
},
"node_modules/dprint-node/node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -10533,6 +10601,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -15964,6 +16033,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -16164,6 +16234,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -17664,6 +17735,42 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-poet": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.12.0.tgz",
"integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"dprint-node": "^1.0.8"
}
},
"node_modules/ts-proto": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.8.0.tgz",
"integrity": "sha512-OtHoiTNYdmtKlkfQZpEVt6wX8wxU2bmHbVNvIopInng0QmzyHapSzLTXKkDToyqJWVNjD18lopERyO64tCBTZQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0",
"case-anything": "^2.1.13",
"ts-poet": "^6.12.0",
"ts-proto-descriptors": "2.0.0"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.0.0.tgz",
"integrity": "sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==",
"dev": true,
"license": "ISC",
"dependencies": {
"@bufbuild/protobuf": "^2.0.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -17753,6 +17860,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17973,6 +18081,7 @@
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -18238,6 +18347,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -18287,6 +18397,7 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -18518,6 +18629,7 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
@@ -18722,6 +18834,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -19055,6 +19168,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -19262,6 +19376,7 @@
"version": "3.29.5",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -19989,6 +20104,7 @@
"version": "3.29.5",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -20151,6 +20267,7 @@
"version": "3.29.5",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
+4 -1
View File
@@ -13,6 +13,7 @@
"devDependencies": {
"@eslint/js": "^9.35.0",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"chalk": "^4.1.0",
@@ -27,6 +28,7 @@
"license-check-and-add": "^4.0.5",
"mocha": "^10.6.0",
"nodemon": "^3.1.0",
"ts-proto": "^2.8.0",
"typescript": "^5.4.5",
"uglify-js": "^3.17.4",
"vite-plugin-static-copy": "^3.1.3",
@@ -44,7 +46,8 @@
"check-translations": "node tools/check-translations.js",
"prepare": "husky",
"build:ts": "tsc",
"postinstall": "npm run build:ts"
"postinstall": "npm run build:ts",
"gen": "./scripts/gen.sh"
},
"workspaces": [
"src/*",
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
protoc \
-I=src/backend/src/filesystem/definitions/proto \
--plugin=protoc-gen-ts_proto=$(npm root)/.bin/protoc-gen-ts_proto \
--ts_proto_out=src/backend/src/filesystem/definitions/ts \
--ts_proto_opt=esModuleInterop=true,outputServices=none,outputJsonMethods=true,useExactTypes=false,snakeToCamel=false \
src/backend/src/filesystem/definitions/proto/fsentry.proto
@@ -0,0 +1,26 @@
syntax = "proto3";
// The FSEntry from client's (puter-js, http API) perspective, it's used for
// - end to end test
// - backend logic
// - communication between servers
message FSEntry {
string uuid = 1;
// Same as uuid, used for backward compatibility.
string uid = 2;
string name = 3;
string path = 4;
string parent_uuid = 5;
// Same as parent_uuid, used for backward compatibility.
string parent_uid = 6;
// Same as parent_uuid, used for backward compatibility.
string parent_id = 7;
bool is_dir = 8;
int64 created = 9;
int64 modified = 10;
int64 accessed = 11;
int64 size = 12;
}
@@ -0,0 +1,256 @@
"use strict";
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.8.0
// protoc v3.21.12
// source: fsentry.proto
Object.defineProperty(exports, "__esModule", { value: true });
exports.FSEntry = exports.protobufPackage = void 0;
/* eslint-disable */
const wire_1 = require("@bufbuild/protobuf/wire");
exports.protobufPackage = "";
function createBaseFSEntry() {
return {
uuid: "",
uid: "",
name: "",
path: "",
parent_uuid: "",
parent_uid: "",
parent_id: "",
is_dir: false,
created: 0,
modified: 0,
accessed: 0,
size: 0,
};
}
exports.FSEntry = {
encode(message, writer = new wire_1.BinaryWriter()) {
if (message.uuid !== "") {
writer.uint32(10).string(message.uuid);
}
if (message.uid !== "") {
writer.uint32(18).string(message.uid);
}
if (message.name !== "") {
writer.uint32(26).string(message.name);
}
if (message.path !== "") {
writer.uint32(34).string(message.path);
}
if (message.parent_uuid !== "") {
writer.uint32(42).string(message.parent_uuid);
}
if (message.parent_uid !== "") {
writer.uint32(50).string(message.parent_uid);
}
if (message.parent_id !== "") {
writer.uint32(58).string(message.parent_id);
}
if (message.is_dir !== false) {
writer.uint32(64).bool(message.is_dir);
}
if (message.created !== 0) {
writer.uint32(72).int64(message.created);
}
if (message.modified !== 0) {
writer.uint32(80).int64(message.modified);
}
if (message.accessed !== 0) {
writer.uint32(88).int64(message.accessed);
}
if (message.size !== 0) {
writer.uint32(96).int64(message.size);
}
return writer;
},
decode(input, length) {
const reader = input instanceof wire_1.BinaryReader ? input : new wire_1.BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseFSEntry();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.uuid = reader.string();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.uid = reader.string();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.name = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.path = reader.string();
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.parent_uuid = reader.string();
continue;
}
case 6: {
if (tag !== 50) {
break;
}
message.parent_uid = reader.string();
continue;
}
case 7: {
if (tag !== 58) {
break;
}
message.parent_id = reader.string();
continue;
}
case 8: {
if (tag !== 64) {
break;
}
message.is_dir = reader.bool();
continue;
}
case 9: {
if (tag !== 72) {
break;
}
message.created = longToNumber(reader.int64());
continue;
}
case 10: {
if (tag !== 80) {
break;
}
message.modified = longToNumber(reader.int64());
continue;
}
case 11: {
if (tag !== 88) {
break;
}
message.accessed = longToNumber(reader.int64());
continue;
}
case 12: {
if (tag !== 96) {
break;
}
message.size = longToNumber(reader.int64());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object) {
return {
uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "",
uid: isSet(object.uid) ? globalThis.String(object.uid) : "",
name: isSet(object.name) ? globalThis.String(object.name) : "",
path: isSet(object.path) ? globalThis.String(object.path) : "",
parent_uuid: isSet(object.parent_uuid) ? globalThis.String(object.parent_uuid) : "",
parent_uid: isSet(object.parent_uid) ? globalThis.String(object.parent_uid) : "",
parent_id: isSet(object.parent_id) ? globalThis.String(object.parent_id) : "",
is_dir: isSet(object.is_dir) ? globalThis.Boolean(object.is_dir) : false,
created: isSet(object.created) ? globalThis.Number(object.created) : 0,
modified: isSet(object.modified) ? globalThis.Number(object.modified) : 0,
accessed: isSet(object.accessed) ? globalThis.Number(object.accessed) : 0,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
};
},
toJSON(message) {
const obj = {};
if (message.uuid !== "") {
obj.uuid = message.uuid;
}
if (message.uid !== "") {
obj.uid = message.uid;
}
if (message.name !== "") {
obj.name = message.name;
}
if (message.path !== "") {
obj.path = message.path;
}
if (message.parent_uuid !== "") {
obj.parent_uuid = message.parent_uuid;
}
if (message.parent_uid !== "") {
obj.parent_uid = message.parent_uid;
}
if (message.parent_id !== "") {
obj.parent_id = message.parent_id;
}
if (message.is_dir !== false) {
obj.is_dir = message.is_dir;
}
if (message.created !== 0) {
obj.created = Math.round(message.created);
}
if (message.modified !== 0) {
obj.modified = Math.round(message.modified);
}
if (message.accessed !== 0) {
obj.accessed = Math.round(message.accessed);
}
if (message.size !== 0) {
obj.size = Math.round(message.size);
}
return obj;
},
create(base) {
return exports.FSEntry.fromPartial(base ?? {});
},
fromPartial(object) {
const message = createBaseFSEntry();
message.uuid = object.uuid ?? "";
message.uid = object.uid ?? "";
message.name = object.name ?? "";
message.path = object.path ?? "";
message.parent_uuid = object.parent_uuid ?? "";
message.parent_uid = object.parent_uid ?? "";
message.parent_id = object.parent_id ?? "";
message.is_dir = object.is_dir ?? false;
message.created = object.created ?? 0;
message.modified = object.modified ?? 0;
message.accessed = object.accessed ?? 0;
message.size = object.size ?? 0;
return message;
},
};
function longToNumber(int64) {
const num = globalThis.Number(int64.toString());
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
}
return num;
}
function isSet(value) {
return value !== null && value !== undefined;
}
//# sourceMappingURL=fsentry.js.map
@@ -0,0 +1,315 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.8.0
// protoc v3.21.12
// source: fsentry.proto
/* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
export const protobufPackage = "";
/**
* The FSEntry from client's (puter-js, http API) perspective, it's used for
* - end to end test
* - backend logic
* - communication between servers
*/
export interface FSEntry {
uuid: string;
/** Same as uuid, used for backward compatibility. */
uid: string;
name: string;
path: string;
parent_uuid: string;
/** Same as parent_uuid, used for backward compatibility. */
parent_uid: string;
/** Same as parent_uuid, used for backward compatibility. */
parent_id: string;
is_dir: boolean;
created: number;
modified: number;
accessed: number;
size: number;
}
function createBaseFSEntry(): FSEntry {
return {
uuid: "",
uid: "",
name: "",
path: "",
parent_uuid: "",
parent_uid: "",
parent_id: "",
is_dir: false,
created: 0,
modified: 0,
accessed: 0,
size: 0,
};
}
export const FSEntry: MessageFns<FSEntry> = {
encode(message: FSEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.uuid !== "") {
writer.uint32(10).string(message.uuid);
}
if (message.uid !== "") {
writer.uint32(18).string(message.uid);
}
if (message.name !== "") {
writer.uint32(26).string(message.name);
}
if (message.path !== "") {
writer.uint32(34).string(message.path);
}
if (message.parent_uuid !== "") {
writer.uint32(42).string(message.parent_uuid);
}
if (message.parent_uid !== "") {
writer.uint32(50).string(message.parent_uid);
}
if (message.parent_id !== "") {
writer.uint32(58).string(message.parent_id);
}
if (message.is_dir !== false) {
writer.uint32(64).bool(message.is_dir);
}
if (message.created !== 0) {
writer.uint32(72).int64(message.created);
}
if (message.modified !== 0) {
writer.uint32(80).int64(message.modified);
}
if (message.accessed !== 0) {
writer.uint32(88).int64(message.accessed);
}
if (message.size !== 0) {
writer.uint32(96).int64(message.size);
}
return writer;
},
decode(input: BinaryReader | Uint8Array, length?: number): FSEntry {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
const end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseFSEntry();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break;
}
message.uuid = reader.string();
continue;
}
case 2: {
if (tag !== 18) {
break;
}
message.uid = reader.string();
continue;
}
case 3: {
if (tag !== 26) {
break;
}
message.name = reader.string();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.path = reader.string();
continue;
}
case 5: {
if (tag !== 42) {
break;
}
message.parent_uuid = reader.string();
continue;
}
case 6: {
if (tag !== 50) {
break;
}
message.parent_uid = reader.string();
continue;
}
case 7: {
if (tag !== 58) {
break;
}
message.parent_id = reader.string();
continue;
}
case 8: {
if (tag !== 64) {
break;
}
message.is_dir = reader.bool();
continue;
}
case 9: {
if (tag !== 72) {
break;
}
message.created = longToNumber(reader.int64());
continue;
}
case 10: {
if (tag !== 80) {
break;
}
message.modified = longToNumber(reader.int64());
continue;
}
case 11: {
if (tag !== 88) {
break;
}
message.accessed = longToNumber(reader.int64());
continue;
}
case 12: {
if (tag !== 96) {
break;
}
message.size = longToNumber(reader.int64());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skip(tag & 7);
}
return message;
},
fromJSON(object: any): FSEntry {
return {
uuid: isSet(object.uuid) ? globalThis.String(object.uuid) : "",
uid: isSet(object.uid) ? globalThis.String(object.uid) : "",
name: isSet(object.name) ? globalThis.String(object.name) : "",
path: isSet(object.path) ? globalThis.String(object.path) : "",
parent_uuid: isSet(object.parent_uuid) ? globalThis.String(object.parent_uuid) : "",
parent_uid: isSet(object.parent_uid) ? globalThis.String(object.parent_uid) : "",
parent_id: isSet(object.parent_id) ? globalThis.String(object.parent_id) : "",
is_dir: isSet(object.is_dir) ? globalThis.Boolean(object.is_dir) : false,
created: isSet(object.created) ? globalThis.Number(object.created) : 0,
modified: isSet(object.modified) ? globalThis.Number(object.modified) : 0,
accessed: isSet(object.accessed) ? globalThis.Number(object.accessed) : 0,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
};
},
toJSON(message: FSEntry): unknown {
const obj: any = {};
if (message.uuid !== "") {
obj.uuid = message.uuid;
}
if (message.uid !== "") {
obj.uid = message.uid;
}
if (message.name !== "") {
obj.name = message.name;
}
if (message.path !== "") {
obj.path = message.path;
}
if (message.parent_uuid !== "") {
obj.parent_uuid = message.parent_uuid;
}
if (message.parent_uid !== "") {
obj.parent_uid = message.parent_uid;
}
if (message.parent_id !== "") {
obj.parent_id = message.parent_id;
}
if (message.is_dir !== false) {
obj.is_dir = message.is_dir;
}
if (message.created !== 0) {
obj.created = Math.round(message.created);
}
if (message.modified !== 0) {
obj.modified = Math.round(message.modified);
}
if (message.accessed !== 0) {
obj.accessed = Math.round(message.accessed);
}
if (message.size !== 0) {
obj.size = Math.round(message.size);
}
return obj;
},
create(base?: DeepPartial<FSEntry>): FSEntry {
return FSEntry.fromPartial(base ?? {});
},
fromPartial(object: DeepPartial<FSEntry>): FSEntry {
const message = createBaseFSEntry();
message.uuid = object.uuid ?? "";
message.uid = object.uid ?? "";
message.name = object.name ?? "";
message.path = object.path ?? "";
message.parent_uuid = object.parent_uuid ?? "";
message.parent_uid = object.parent_uid ?? "";
message.parent_id = object.parent_id ?? "";
message.is_dir = object.is_dir ?? false;
message.created = object.created ?? 0;
message.modified = object.modified ?? 0;
message.accessed = object.accessed ?? 0;
message.size = object.size ?? 0;
return message;
},
};
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString());
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
}
return num;
}
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
export interface MessageFns<T> {
encode(message: T, writer?: BinaryWriter): BinaryWriter;
decode(input: BinaryReader | Uint8Array, length?: number): T;
fromJSON(object: any): T;
toJSON(message: T): unknown;
create(base?: DeepPartial<T>): T;
fromPartial(object: DeepPartial<T>): T;
}
-2863
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -1,20 +0,0 @@
{
"name": "puter-integration-tests",
"version": "1.0.0",
"description": "Integration tests for Puter",
"main": "index.js",
"scripts": {
"test": "mocha captcha/**/*.test.js",
"test:auth": "mocha captcha/authentication-flow.test.js",
"test:ui": "mocha captcha/ui-behavior.test.js"
},
"devDependencies": {
"chai": "^4.3.7",
"express": "^4.18.2",
"jsdom": "^21.1.0",
"mocha": "^10.2.0",
"sinon": "^15.2.0",
"body-parser": "^1.20.2",
"supertest": "^6.3.3"
}
}
+49
View File
@@ -0,0 +1,49 @@
## Table of Contents
- [Summary](#summary)
- [How to use](#how-to-use)
- [Initialize the Client Config](#initialize-the-client-config)
- [Run API-Tester (test http API)](#run-api-tester-test-http-api)
- [Run Playwright (test puter-js API with browser environment)](#run-playwright-test-puter-js-api-with-browser-environment)
- [Run Vitest (test puter-js API with node environment)](#run-vitest-test-puter-js-api-with-node-environment)
## Summary
End-to-end tests for puter-js and http API.
## How to use
### Initialize the Client Config
1. Start a backend server:
```bash
npm start
```
2. Copy `example-client-config.yaml` and edit the `auth_token` field. (`auth_token` can be obtained by logging in on the webpage and typing `puter.authToken` in Developer Tools's console)
```bash
cp ./tests/example-client-config.yaml ./tests/client-config.yaml
```
### Run API-Tester (test http API)
```bash
node ./tests/api-tester/apitest.js --unit --stop-on-failure
```
### Run Playwright (test puter-js API with browser environment)
```bash
cd ./tests/playwright
npm install
npx playwright install --with-deps
npx playwright test
```
### Run Vitest (test puter-js API with node environment)
```bash
npm run test:puterjs-api
```
@@ -16,7 +16,7 @@ try {
options: {
config: {
type: 'string',
default: './tools/api-tester/config.yml',
default: './tests/client-config.yaml',
},
report: {
type: 'string',
@@ -20,10 +20,10 @@ module.exports = class TestSDK {
this.httpsAgent = new https.Agent({
rejectUnauthorized: false
})
const url_origin = new url.URL(conf.url).origin;
const url_origin = new url.URL(conf.api_url).origin;
this.headers_ = {
'Origin': url_origin,
'Authorization': `Bearer ${conf.token}`
'Authorization': `Bearer ${conf.auth_token}`
};
this.installAPIMethodShorthands_();
@@ -332,7 +332,7 @@ module.exports = class TestSDK {
}
getURL (...path) {
const apiURL = new url.URL(this.conf.url);
const apiURL = new url.URL(this.conf.api_url);
apiURL.pathname = path_.posix.join(
apiURL.pathname,
...path
+79
View File
@@ -0,0 +1,79 @@
#! /usr/bin/env python3
#
# Usage:
# ./tools/api-tester/ci/run.py
import time
import os
import json
import requests
import yaml
import cxc_toolkit
import common
def update_server_config():
# Load the config file
config_file = f"{os.getcwd()}/volatile/config/config.json"
with open(config_file, "r") as f:
config = json.load(f)
# Ensure services and mountpoint sections exist
if "services" not in config:
config["services"] = {}
if "mountpoint" not in config["services"]:
config["services"]["mountpoint"] = {}
if "mountpoints" not in config["services"]["mountpoint"]:
config["services"]["mountpoint"]["mountpoints"] = {}
# Add the mountpoint configuration
mountpoint_config = {
"/": {"mounter": "puterfs"},
"/admin/tmp": {"mounter": "memoryfs"},
}
# Merge mountpoints (overwrite existing ones)
config["services"]["mountpoint"]["mountpoints"].update(mountpoint_config)
# Write the updated config back
with open(config_file, "w") as f:
json.dump(config, f, indent=2)
def run():
# =========================================================================
# free the port 4100
# =========================================================================
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
# =========================================================================
# config server
# =========================================================================
cxc_toolkit.exec.run_command("npm install")
common.init_backend_config()
admin_password = common.get_admin_password()
update_server_config()
# =========================================================================
# config client
# =========================================================================
cxc_toolkit.exec.run_background("npm start")
# wait 10s for the server to start
time.sleep(10)
token = common.get_token(admin_password)
common.init_client_config(token)
# =========================================================================
# run the test
# =========================================================================
cxc_toolkit.exec.run_command(
"node ./tests/api-tester/apitest.js --unit --stop-on-failure"
)
if __name__ == "__main__":
run()
+94
View File
@@ -0,0 +1,94 @@
import os
import time
import cxc_toolkit
import requests
import yaml
PUTER_ROOT = os.getcwd()
def init_backend_config():
"""
Initialize a default config in ./volatile/config/config.json.
"""
# init config.json
server_process = cxc_toolkit.exec.run_background("npm start")
# wait 10s for the server to start
time.sleep(10)
server_process.terminate()
# Possible reasons for failure:
# - The backend server is not initialized, run "npm start" to initialize it.
# - Admin password in the kv service is flushed, have to trigger the creation of the admin user.
# 1. sqlite3 ./volatile/runtime/puter-database.sqlite
# 2. DELETE FROM user WHERE username = 'admin';
def get_admin_password() -> str:
"""
Get the admin password from the backend server, throw an error if not found.
"""
LOG_PATH = "/tmp/backend.log"
backend_process = cxc_toolkit.exec.run_background("npm start", log_path=LOG_PATH)
# NB: run_command + kill_on_output may wait indefinitely, use run_background + hard limit instead
time.sleep(10)
backend_process.terminate()
# read the log file
with open(LOG_PATH, "r") as f:
lines = f.readlines()
for line in lines:
if "password for admin" in line:
print(f"found password line: ---{line}---")
admin_password = line.split("password for admin is:")[1].strip()
print(f"Extracted admin password: {admin_password}")
return admin_password
raise RuntimeError(f"no admin password found, check {LOG_PATH} for details")
def get_token(admin_password: str) -> str:
"""
Get the token from the backend server, throw an error if not found.
"""
server_url = "http://api.puter.localhost:4100/login"
login_data = {"username": "admin", "password": admin_password}
response = requests.post(
server_url,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Origin": "http://api.puter.localhost:4100",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
},
json=login_data,
timeout=30,
)
response_json = response.json()
if "token" not in response_json:
raise RuntimeError("No token found")
return response_json["token"]
def init_client_config(token: str):
"""
Initialize a client config in ./tests/client-config.yaml.
"""
example_config_path = f"{PUTER_ROOT}/tests/example-client-config.yaml"
config_path = f"{PUTER_ROOT}/tests/client-config.yaml"
# load
with open(example_config_path, "r") as f:
config = yaml.safe_load(f)
# update
config["auth_token"] = token
# write
with open(config_path, "w") as f:
yaml.dump(config, f, default_flow_style=False, indent=2)
+143
View File
@@ -0,0 +1,143 @@
#! /usr/bin/env python3
# test the client-replica feature
# - need browser environment (following features require browser environment: fs naive-cache, client-replica, wspush)
# - test multi-server setup
# - test change-propagation-time
# - test local read
# - test consistency
# first stage: test in the existing workspace, test single server + multiple sessions
# second stage: test from a fresh clone, test single server + multiple sessions
# third stage: test in the existing workspace, test multiple servers + multiple sessions
# fourth stage: test from a fresh clone, test multiple servers + multiple sessions
import time
import os
import json
import requests
import yaml
import cxc_toolkit
import common
ENABLE_FS_TREE_MANAGER = False
PUTER_ROOT = common.PUTER_ROOT
def init_backend_config():
"""
TODO: replace with common.init_backend_config
"""
# init config.json
server_process = cxc_toolkit.exec.run_background("npm start")
# wait 10s for the server to start
time.sleep(10)
server_process.terminate()
example_config_path = f"{PUTER_ROOT}/volatile/config/config.json"
config_path = f"{PUTER_ROOT}/volatile/config/config.json"
# load
with open(example_config_path, "r") as f:
config = json.load(f)
# update
if ENABLE_FS_TREE_MANAGER:
config["services"]["client-replica"] = {
"enabled": True,
"fs_tree_manager_url": "localhost:50052",
}
# write
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
def init_fs_tree_manager_config():
example_config_path = f"{PUTER_ROOT}/src/fs_tree_manager/example-config.yaml"
config_path = f"{PUTER_ROOT}/src/fs_tree_manager/config.yaml"
# load
with open(example_config_path, "r") as f:
config = yaml.safe_load(f)
# update
config["database"]["driver"] = "sqlite3"
config["database"]["sqlite3"][
"path"
] = f"{PUTER_ROOT}/volatile/runtime/puter-database.sqlite"
# write
with open(config_path, "w") as f:
yaml.dump(config, f, default_flow_style=False, indent=2)
print(f"fs-tree-manager config initialized at {config_path}")
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")
init_backend_config()
admin_password = common.get_admin_password()
# =========================================================================
# start backend server
# =========================================================================
cxc_toolkit.exec.run_background(
"npm start", 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)
# =========================================================================
# start fs-tree-manager server
# =========================================================================
if ENABLE_FS_TREE_MANAGER:
init_fs_tree_manager_config()
cxc_toolkit.exec.run_command(
"go mod download",
work_dir=f"{PUTER_ROOT}/src/fs_tree_manager",
)
cxc_toolkit.exec.run_background(
"go run server.go",
work_dir=f"{PUTER_ROOT}/src/fs_tree_manager",
log_path="/tmp/fs-tree-manager.log",
)
# NB: "go mod download" and "go run server.go" may take a long time in github
# action environment, I don't know why.
time.sleep(60)
# =========================================================================
# run the test
# =========================================================================
cxc_toolkit.exec.run_command(
"npx playwright test",
work_dir=f"{PUTER_ROOT}/tests/playwright",
)
if __name__ == "__main__":
run()
@@ -1,3 +1,3 @@
cxc-toolkit>=0.9.2
cxc-toolkit>=1.0.0
requests==2.32.4
PyYAML==6.0.2
+53
View File
@@ -0,0 +1,53 @@
#! /usr/bin/env python3
import time
import cxc_toolkit
import common
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_background(
"npm start", work_dir=common.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(
"npm run test:puterjs-api",
work_dir=common.PUTER_ROOT,
)
if __name__ == "__main__":
run()
+9
View File
@@ -0,0 +1,9 @@
api_url: http://api.puter.localhost:4100
frontend_url: http://puter.localhost:4100
username: admin
auth_token: <your-token>
mountpoints:
- path: /
provider: puterfs
- path: /admin/tmp
provider: memoryfs
-20
View File
@@ -1,20 +0,0 @@
{
"name": "puter-integration-tests",
"version": "1.0.0",
"description": "Integration tests for Puter",
"main": "index.js",
"scripts": {
"test": "mocha captcha/**/*.test.js",
"test:auth": "mocha captcha/authentication-flow.test.js",
"test:ui": "mocha captcha/ui-behavior.test.js"
},
"devDependencies": {
"chai": "^4.3.7",
"express": "^4.18.2",
"jsdom": "^21.1.0",
"mocha": "^10.2.0",
"sinon": "^15.2.0",
"body-parser": "^1.20.2",
"supertest": "^6.3.3"
}
}
+27
View File
@@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
+8
View File
@@ -0,0 +1,8 @@
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
+32
View File
@@ -0,0 +1,32 @@
import * as fs from 'fs'
import * as path from 'path'
import * as yaml from 'yaml'
// Strong-typed configuration interface
export interface TestConfig {
api_url: string
frontend_url: string
username: string
auth_token: string
}
// Singleton configuration loader - loads config only once
let config: TestConfig | null = null
export function getTestConfig(): TestConfig {
if (config === null) {
const configPath = path.join(__dirname, '../../client-config.yaml')
const rawConfig = yaml.parse(fs.readFileSync(configPath, 'utf8'))
// Validate required fields
if (!rawConfig.api_url || !rawConfig.frontend_url || !rawConfig.username || !rawConfig.auth_token) {
throw new Error('Invalid test configuration: missing required fields')
}
config = rawConfig as TestConfig
}
return config
}
// Export the typed configuration
export const testConfig: TestConfig = getTestConfig()
+16
View File
@@ -0,0 +1,16 @@
{
"name": "playwright",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.56.0",
"@types/node": "^24.7.2",
"yaml": "^2.4.5"
}
}
+81
View File
@@ -0,0 +1,81 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
// Disable parallelism since puter fs doesn't provide concurrent safety.
workers: 1,
/* 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',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
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,
// },
});
@@ -0,0 +1 @@
// puter.fs.batch doesn't work well, add tests later.
@@ -0,0 +1,232 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('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`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
}, { testPath, sourceFile, destDir });
// Copy file
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.copy(sourceFile, destDir);
return result;
} catch (error) {
console.error('copy error:', error);
return null;
}
}, { sourceFile, destDir });
console.log('result: ', result);
expect(result[0]).toBeTruthy();
expect(result[0].copied.name).toBe('a_file.txt');
});
test('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`;
const newName = 'x_renamed';
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
}, { testPath, sourceFile, destDir });
// Copy file with new name
const result = await page.evaluate(async ({ sourceFile, destDir, newName }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.copy(sourceFile, destDir, { newName });
return result;
} catch (error) {
console.error('copy error:', error);
return null;
}
}, { sourceFile, destDir, newName });
expect(result).toBeTruthy();
expect(result[0].copied.name).toBe(newName);
});
test('copy file with overwrite', async ({ page }) => {
const testPath = `${BASE_PATH}/copy_cart_3`;
const sourceFile = `${testPath}/a/a_file.txt`;
const destDir = `${testPath}/b`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
}, { testPath, sourceFile, destDir });
// Copy file with overwrite
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.copy(sourceFile, destDir, { overwrite: true });
return result;
} catch (error) {
console.error('copy error:', error);
return null;
}
}, { sourceFile, destDir });
expect(result).toBeTruthy();
expect(result[0]).toBeTruthy();
});
test('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`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
}, { testPath, sourceFile, destDir });
// Attempt copy without overwrite (should fail)
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.copy(sourceFile, destDir);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code, entry_name: error.entry_name };
}
}, { sourceFile, destDir });
expect(result.success).toBe(false);
expect(result.code).toBeTruthy();
});
test('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`;
// Setup: create file as destination (not directory)
await page.evaluate(async ({ testPath, sourceFile, destFile }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.write(destFile, 'placeholder\n');
}, { testPath, sourceFile, destFile });
// Attempt copy with specified name to file destination (should error)
const result = await page.evaluate(async ({ sourceFile, destFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.copy(sourceFile, destFile, { newName: 'x_renamed' });
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { sourceFile, destFile });
expect(result.success).toBe(false);
expect(result.code).toBe('dest_is_not_a_directory');
});
test('copy empty directory', async ({ page }) => {
const testPath = `${BASE_PATH}/copy_cart_7`;
const sourceDir = `${testPath}/a/a_directory`;
const destDir = `${testPath}/b`;
// Setup: create empty directory
await page.evaluate(async ({ testPath, sourceDir, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.mkdir(sourceDir);
await puter.fs.mkdir(destDir);
}, { testPath, sourceDir, destDir });
// Copy directory
const result = await page.evaluate(async ({ sourceDir, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.copy(sourceDir, destDir);
return result;
} catch (error) {
console.error('copy error:', error);
return null;
}
}, { sourceDir, destDir });
expect(result).toBeTruthy();
expect(result[0].copied.name).toBe('a_directory');
});
test('copy full directory', async ({ page }) => {
const testPath = `${BASE_PATH}/copy_cart_8`;
const sourceDir = `${testPath}/a/a_directory`;
const destDir = `${testPath}/b`;
// Setup: create full directory with file, empty dir, and nested dir
await page.evaluate(async ({ testPath, sourceDir, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.mkdir(sourceDir);
await puter.fs.write(`${sourceDir}/a_file.txt`, 'file a contents\n');
await puter.fs.mkdir(`${sourceDir}/b_directory`);
await puter.fs.write(`${sourceDir}/b_directory/b_file.txt`, 'file b contents\n');
await puter.fs.mkdir(`${sourceDir}/c_directory`);
await puter.fs.mkdir(destDir);
}, { testPath, sourceDir, destDir });
// Copy directory
const result = await page.evaluate(async ({ sourceDir, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.copy(sourceDir, destDir);
return result;
} catch (error) {
console.error('copy error:', error);
return null;
}
}, { sourceDir, destDir });
expect(result).toBeTruthy();
expect(result[0].copied.name).toBe('a_directory');
// Verify nested files were copied
const nestedFile = await page.evaluate(async ({ destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.read(`${destDir}/a_directory/a_file.txt`);
return result.text();
} catch (error) {
return null;
}
}, { destDir });
expect(nestedFile).toBe('file a contents\n');
});
@@ -0,0 +1,115 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('delete for normal file', async ({ page }) => {
const testPath = `${BASE_PATH}/delete_test_1`;
const testFile = `${testPath}/test_delete.txt`;
await page.evaluate(async ({ testPath, testFile }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.write(testFile, 'delete test\n');
}, { testPath, testFile });
await page.evaluate(async ({ testFile }) => {
const puter = (window as any).puter;
await puter.fs.delete(testFile);
}, { testFile });
let threw = false;
const result = await page.evaluate(async ({ testFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(testFile);
return { exists: true };
} catch (e) {
return { exists: false, error: (e as any).code || (e as any).message };
}
}, { testFile });
expect(result.exists).toBe(false);
});
test('error for non-existing file', async ({ page }) => {
const testPath = `${BASE_PATH}/delete_test_2`;
const testFile = `${testPath}/test_delete.txt`;
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
let threw = false;
const result = await page.evaluate(async ({ testFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.delete(testFile);
return { success: true };
} catch (e) {
return { success: false, error: (e as any).code || (e as any).message };
}
}, { testFile });
expect(result.success).toBe(false);
});
test('delete for directory', async ({ page }) => {
const testPath = `${BASE_PATH}/delete_test_3`;
const testDir = `${testPath}/test_delete_dir`;
await page.evaluate(async ({ testPath, testDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(testDir);
}, { testPath, testDir });
await page.evaluate(async ({ testDir }) => {
const puter = (window as any).puter;
await puter.fs.delete(testDir);
}, { testDir });
const result = await page.evaluate(async ({ testDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(testDir);
return { exists: true };
} catch (e) {
return { exists: false, error: (e as any).code || (e as any).message };
}
}, { testDir });
expect(result.exists).toBe(false);
});
test('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`;
await page.evaluate(async ({ testPath, testDir, testFile }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(testDir);
await puter.fs.write(testFile, 'delete test\n');
}, { testPath, testDir, testFile });
await page.evaluate(async ({ testDir }) => {
const puter = (window as any).puter;
await puter.fs.delete(testDir, { recursive: true });
}, { testDir });
// Wait for deletion to complete
await page.waitForTimeout(500);
const result = await page.evaluate(async ({ testDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(testDir);
return { exists: true };
} catch (e) {
return { exists: false, error: (e as any).code || (e as any).message };
}
}, { testDir });
expect(result.exists).toBe(false);
});
@@ -0,0 +1,123 @@
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';
// The maximum time needed for file-system change to be propagated from
// one session to others.
export const CHANGE_PROPAGATION_TIME = 0;
export const BASE_PATH = '/admin/tests';
export const ERROR_CODES = [
'forbidden',
'dest_does_not_exist',
'subject_does_not_exist',
'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);
await page.evaluate(async ({ BASE_PATH }) => {
const puter = (window as any).puter;
try {
await puter.fs.delete(BASE_PATH, { recursive: true });
} catch( error ) {
// ignore error
console.error('delete error:', error);
}
try {
await puter.fs.mkdir(BASE_PATH);
} catch( error ) {
console.error('mkdir error:', error);
throw error;
}
}, { BASE_PATH });
await use(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()]);
});
@@ -0,0 +1,165 @@
import { expect } from '@playwright/test';
import { BASE_PATH, ERROR_CODES, test } 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 }) => {
// 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 }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.mkdir(path, {
createMissingParents: true,
});
console.log('mkdir result?', result);
return result;
} catch (error) {
console.error('error?', error);
return null;
}
}, { path });
console.log('result?', result);
});
test('mkdir dedupe name', async ({ page }) => {
const basePath = `${BASE_PATH}/dedupe_test`;
// Create initial directory
await page.evaluate(async ({ basePath }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(basePath);
} catch (error) {
console.error('error: ', error);
}
}, { basePath });
// Test dedupe functionality
for (let i = 1; i <= 3; i++) {
const result = await page.evaluate(async ({ basePath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.mkdir(basePath, { dedupeName: true });
return result;
} catch (error) {
console.error('mkdir error:', error);
return null;
}
}, { basePath });
if (result) {
expect(result.name).toBe(`dedupe_test (${i})`);
}
// Verify the directory exists
const stat = await page.evaluate(async ({ basePath, i }) => {
const puter = (window as any).puter;
try {
const stat = await puter.fs.stat(`${basePath} (${i})`);
return stat;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { basePath, i });
if (stat) {
expect(stat.name).toBe(`dedupe_test (${i})`);
}
}
});
test('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;
try {
await puter.fs.mkdir('/a');
return null;
} catch (error: any) {
return error.code;
}
});
expect(ERROR_CODES.includes(error_code)).toBe(true);
// Test parent + path format
error_code = await page.evaluate(async () => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir('a', { parent: '/' });
return null;
} catch (error: any) {
return error.code;
}
});
expect(ERROR_CODES.includes(error_code)).toBe(true);
});
test('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`;
// Verify parent directory does not exist initially
let error_code = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(`${testPath}/a`);
return null;
} catch (error: any) {
console.error('stat error:', error);
return error.code;
}
}, { testPath });
expect(ERROR_CODES.includes(error_code)).toBe(true);
// Test mkdir with create_missing_parents
const result = await page.evaluate(async ({ targetPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.mkdir(targetPath, {
createMissingParents: true,
});
return result;
} catch (error) {
console.error('mkdir error:', error);
return null;
}
}, { targetPath });
expect(result).toBeTruthy();
expect(result.name).toBe('c');
// Test mkdir without create_missing_parents should fail
error_code = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(`${testPath}/x/y/z`);
return null;
} catch (error: any) {
return error.code;
}
}, { testPath });
expect(ERROR_CODES.includes(error_code)).toBe(true);
// Verify all directories along the path exist
const paths = ['a', 'a/b', 'a/b/c'];
for (const path of paths) {
const stat = await page.evaluate(async ({ testPath, path }) => {
const puter = (window as any).puter;
try {
const stat = await puter.fs.stat(`${testPath}/${path}`);
return stat;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath, path });
expect(stat).toBeTruthy();
expect(stat.name).toBe(path.split('/').pop());
}
});
@@ -0,0 +1,205 @@
import { expect } from '@playwright/test';
import { BASE_PATH, ERROR_CODES, test } from './fixtures';
test('move file', async ({ page }) => {
const sourceFile = `${BASE_PATH}/just_a_file.txt`;
const targetFile = `${BASE_PATH}/just_a_file_moved.txt`;
// Create source file
await page.evaluate(async ({ sourceFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(sourceFile, 'move test\n');
} catch (error) {
console.error('write error:', error);
}
}, { sourceFile });
// Move the file
const result = await page.evaluate(async ({ sourceFile, targetFile }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceFile, targetFile);
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceFile, targetFile });
expect(result).toBeTruthy();
// Verify target file exists
const movedStat = await page.evaluate(async ({ targetFile }) => {
const puter = (window as any).puter;
try {
const stat = await puter.fs.stat(targetFile);
return stat;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { targetFile });
expect(movedStat).toBeTruthy();
expect(movedStat.name).toBe('just_a_file_moved.txt');
// Verify source file no longer exists
const sourceError = await page.evaluate(async ({ sourceFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(sourceFile);
return null;
} catch (error: any) {
return error.code;
}
}, { sourceFile });
expect(ERROR_CODES.includes(sourceError)).toBe(true);
});
test('move file to existing file', async ({ page }) => {
const sourceFile = `${BASE_PATH}/just_a_file.txt`;
const targetFile = `${BASE_PATH}/dir_with_contents/a.txt`;
// Setup: create source file and target file
await page.evaluate(async ({ sourceFile, targetFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents`);
await puter.fs.write(sourceFile, 'move test\n');
await puter.fs.write(targetFile, 'existing content\n');
} catch (error) {
console.error('setup error:', error);
}
}, { sourceFile, targetFile });
// Attempt to move file to existing file (should fail)
const errorCode = await page.evaluate(async ({ sourceFile, targetFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.move(sourceFile, targetFile);
return null;
} catch (error: any) {
return error.code;
}
}, { sourceFile, targetFile });
expect(ERROR_CODES.includes(errorCode), `unexpected error code: ${errorCode}`).toBe(true);
});
test('move directory', async ({ page }) => {
const sourceDir = `${BASE_PATH}/dir_no_contents`;
const targetDir = `${BASE_PATH}/dir_no_contents_moved`;
// Create source directory
await page.evaluate(async ({ sourceDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(sourceDir);
} catch (error) {
console.error('mkdir error:', error);
}
}, { sourceDir });
// Move the directory
const result = await page.evaluate(async ({ sourceDir, targetDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceDir, targetDir);
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceDir, targetDir });
expect(result).toBeTruthy();
// Verify target directory exists
const movedStat = await page.evaluate(async ({ targetDir }) => {
const puter = (window as any).puter;
try {
const stat = await puter.fs.stat(targetDir);
return stat;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { targetDir });
expect(movedStat).toBeTruthy();
expect(movedStat.name).toBe('dir_no_contents_moved');
// Verify source directory no longer exists
const sourceError = await page.evaluate(async ({ sourceDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(sourceDir);
return null;
} catch (error: any) {
return error.code;
}
}, { sourceDir });
expect(ERROR_CODES.includes(sourceError)).toBe(true);
});
test('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`;
// Setup: create source file and parent directories
await page.evaluate(async ({ BASE_PATH, sourceFile }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents`);
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents/q`);
await puter.fs.mkdir(`${BASE_PATH}/dir_with_contents/w`);
await puter.fs.write(sourceFile, 'move test\n');
}, { BASE_PATH, sourceFile });
// Move file with create_missing_parents
const result = await page.evaluate(async ({ sourceFile, targetFile }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceFile, targetFile, {
createMissingParents: true,
});
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceFile, targetFile });
expect(result).toBeTruthy();
expect(result.parent_dirs_created.length).toBe(2);
// Verify target file exists
const movedStat = await page.evaluate(async ({ targetFile }) => {
const puter = (window as any).puter;
try {
const stat = await puter.fs.stat(targetFile);
return stat;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { targetFile });
expect(movedStat).toBeTruthy();
expect(movedStat.name).toBe('just_a_file.txt');
// Verify source file no longer exists
const sourceError = await page.evaluate(async ({ sourceFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.stat(sourceFile);
return null;
} catch (error: any) {
return error.code;
}
}, { sourceFile });
expect(ERROR_CODES.includes(sourceError)).toBe(true);
});
@@ -0,0 +1,187 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('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`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
}, { testPath, sourceFile, destDir });
// Move file
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceFile, destDir);
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceFile, destDir });
expect(result).toBeTruthy();
expect(result.moved.name).toBe('a_file.txt');
});
test('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`;
const newName = 'x_file.txt';
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
}, { testPath, sourceFile, destDir });
// Move file with new name
const result = await page.evaluate(async ({ sourceFile, destDir, newName }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceFile, destDir, { newName });
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceFile, destDir, newName });
expect(result).toBeTruthy();
expect(result.moved.name).toBe(newName);
});
test('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`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
}, { testPath, sourceFile, destDir });
// Move file with overwrite
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.move(sourceFile, destDir, { overwrite: true });
return result;
} catch (error) {
console.error('move error:', error);
return null;
}
}, { sourceFile, destDir });
expect(result).toBeTruthy();
});
test('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`;
// Setup: create directory structure
await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
await puter.fs.write(`${destDir}/a_file.txt`, 'existing file\n');
}, { testPath, sourceFile, destDir });
// Attempt move without overwrite (should fail)
const result = await page.evaluate(async ({ sourceFile, destDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.move(sourceFile, destDir);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { sourceFile, destDir });
expect(result.success).toBe(false);
expect(result.code).toBeTruthy();
});
test('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`;
// Setup: create file as destination (not directory)
await page.evaluate(async ({ testPath, sourceFile, destFile }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.write(destFile, 'placeholder\n');
}, { testPath, sourceFile, destFile });
// Attempt move with specified name to file destination (should error)
const result = await page.evaluate(async ({ sourceFile, destFile }) => {
const puter = (window as any).puter;
try {
await puter.fs.move(sourceFile, destFile, { newName: 'x_file.txt' });
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { sourceFile, destFile });
expect(result.success).toBe(false);
expect(result.code).toBe('dest_is_not_a_directory');
});
test('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`;
// Setup and get UIDs
const { sourceUID, destUID } = await page.evaluate(async ({ testPath, sourceFile, destDir }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
await puter.fs.mkdir(`${testPath}/a`);
await puter.fs.write(sourceFile, 'file a contents\n');
await puter.fs.mkdir(destDir);
const sourceStat = await puter.fs.stat(sourceFile);
const destStat = await puter.fs.stat(destDir);
return { sourceUID: sourceStat.uid, destUID: destStat.uid };
}, { testPath, sourceFile, destDir });
// Move using UIDs (if supported by puter-js)
const result = await page.evaluate(async ({ sourceUID, destUID }) => {
const puter = (window as any).puter;
try {
// Note: puter-js move might not support uid format directly
// This would require internal API usage
return { sourceUID, destUID };
} catch (error: any) {
return { success: false, error: error.message };
}
}, { sourceUID, destUID });
expect(result.sourceUID).toBeTruthy();
expect(result.destUID).toBeTruthy();
});
@@ -0,0 +1,99 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('readdir test', async ({ page }) => {
// Create test directory
const testDir = `${BASE_PATH}/test_readdir`;
await page.evaluate(async ({ testDir }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(testDir, { overwrite: true });
} catch (error) {
console.error('mkdir error:', error);
throw error;
}
}, { testDir });
// Create files
const files = ['a.txt', 'b.txt', 'c.txt'];
const dirs = ['q', 'w', 'e'];
for (const file of files) {
await page.evaluate(async ({ testDir, file }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(`${testDir}/${file}`, 'readdir test\n', { overwrite: true });
} catch (error) {
console.error(`write error for ${file}:`, error);
throw error;
}
}, { testDir, file });
}
// Create directories
for (const dir of dirs) {
await page.evaluate(async ({ testDir, dir }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(`${testDir}/${dir}`, { overwrite: true });
} catch (error) {
console.error(`mkdir error for ${dir}:`, error);
throw error;
}
}, { testDir, dir });
}
// Verify files
for (const file of files) {
const result = await page.evaluate(async ({ testDir, file }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat(`${testDir}/${file}`);
return result;
} catch (error) {
console.error(`stat error for ${file}:`, error);
return null;
}
}, { testDir, file });
expect(result).toBeTruthy();
expect(result.name).toBe(file);
expect(result.is_dir).toBe(false);
}
// Verify directories
for (const dir of dirs) {
const result = await page.evaluate(async ({ testDir, dir }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat(`${testDir}/${dir}`);
return result;
} catch (error) {
console.error(`stat error for ${dir}:`, error);
return null;
}
}, { testDir, dir });
expect(result).toBeTruthy();
expect(result.name).toBe(dir);
expect(result.is_dir).toBe(true);
}
});
test('readdir of root shouldn\'t return everything', async ({ page }) => {
const result = await page.evaluate(async () => {
const puter = (window as any).puter;
try {
const result = await puter.fs.readdir('/', { recursive: true });
console.log('result?', result);
return result;
} catch (error) {
console.error('readdir error:', error);
return null;
}
});
console.log('result?', result);
});
@@ -0,0 +1,242 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('stat with path (no flags)', async ({ page }) => {
const TEST_FILENAME = 'test_stat.txt';
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
// Write the test file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
} catch (error) {
console.error('write error:', error);
throw error;
}
}, { testPath });
// Stat the file
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat(testPath);
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat.txt');
expect(result.is_dir).toBe(false);
expect(result.uid).toBeDefined();
});
test('stat with uid', async ({ page }) => {
const TEST_FILENAME = 'test_stat.txt';
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
// Write the test file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
} catch (error) {
console.error('write error:', error);
throw error;
}
}, { testPath });
// Get uid from first stat
const firstStat = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat(testPath);
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
const uid = firstStat.uid;
// Stat using uid
const result = await page.evaluate(async ({ uid }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat(uid);
return result;
} catch (error) {
console.error('statu error:', error);
return null;
}
}, { uid });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat.txt');
expect(result.uid).toBe(uid);
});
test('stat with no path or uid provided fails', async ({ page }) => {
const result = await page.evaluate(async () => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat('');
return { success: true, result };
} catch (error: any) {
return { success: false, error: error.message };
}
});
expect(result.success).toBe(false);
});
test('stat with versions', async ({ page }) => {
const TEST_FILENAME = 'test_stat.txt';
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
// Write the test file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
} catch (error) {
console.error('write error:', error);
throw error;
}
}, { testPath });
// Stat with returnVersions flag
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
console.log('STAT WITH VERSIONS', testPath);
const result = await puter.fs.stat({
path: testPath,
returnVersions: true,
});
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat.txt');
console.log('RESULT', result);
expect(Array.isArray(result.versions)).toBe(true);
});
test('stat with shares', async ({ page }) => {
const TEST_FILENAME = 'test_stat.txt';
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
// Write the test file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
} catch (error) {
console.error('write error:', error);
throw error;
}
}, { testPath });
// Stat with returnPermissions flag (returns shares info)
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat({
path: testPath,
returnPermissions: true,
});
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat.txt');
// returnPermissions returns shares info
expect('shares' in result).toBe(true);
expect(Array.isArray(result.shares.users)).toBe(true);
expect(Array.isArray(result.shares.apps)).toBe(true);
});
test('stat with subdomains', async ({ page }) => {
const dirName = 'test_stat_subdomains';
const testPath = `${BASE_PATH}/${dirName}`;
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.mkdir(testPath, { overwrite: true });
} catch (error) {
console.error('mkdir error:', error);
throw error;
}
}, { testPath });
// Stat with returnSubdomains flag
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat({
path: testPath,
returnSubdomains: true,
});
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat_subdomains');
expect(Array.isArray(result.subdomains)).toBe(true);
console.log('RESULT', result);
});
test('stat with size', async ({ page }) => {
const TEST_FILENAME = 'test_stat.txt';
const testPath = `${BASE_PATH}/${TEST_FILENAME}`;
// Write the test file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'stat test\n', { overwrite: true });
} catch (error) {
console.error('write error:', error);
throw error;
}
}, { testPath });
// Stat with returnSize flag
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.stat({
path: testPath,
returnSize: true,
});
return result;
} catch (error) {
console.error('stat error:', error);
return null;
}
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('test_stat.txt');
console.log('RESULT', result);
});
@@ -0,0 +1,130 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('read matches what was written', async ({ page }) => {
const fileName = 'test_rw.txt';
const testPath = `${BASE_PATH}/${fileName}`;
// Write file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.write(testPath, 'example\n');
}, { testPath });
// Read and verify
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const result = await puter.fs.read(testPath);
return await result.text();
}, { testPath });
expect(result).toBe('example\n');
});
test('write without overwrite creates deduped name', async ({ page }) => {
const fileName = 'test_rw.txt';
const testPath = `${BASE_PATH}/${fileName}`;
// Write initial file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.write(testPath, 'example\n');
}, { testPath });
// Write without overwrite - should create deduped name
let errorThrown = false;
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
await puter.fs.write(testPath, 'no-change\n');
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { testPath });
// Note: puter-js behavior might auto-dedupe names
expect(result).toBeTruthy();
});
test('write with overwrite updates file', async ({ page }) => {
const fileName = 'test_rw.txt';
const testPath = `${BASE_PATH}/${fileName}`;
// Write initial file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.write(testPath, 'example\n');
}, { testPath });
// Write with overwrite
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.write(testPath, 'yes-change\n', { overwrite: true });
}, { testPath });
// Verify content was updated
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const result = await puter.fs.read(testPath);
return await result.text();
}, { testPath });
expect(result).toBe('yes-change\n');
});
test('read with version id', async ({ page }) => {
const fileName = 'test_rw.txt';
const testPath = `${BASE_PATH}/${fileName}`;
// Write file
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.write(testPath, 'yes-change\n', { overwrite: true });
}, { testPath });
// Read with version_id
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
try {
const result = await puter.fs.read(testPath, { version_id: '1' });
const text = await result.text();
return { success: true, text };
} catch (error: any) {
return { success: false, error: error.message };
}
}, { testPath });
expect(result.success).toBe(true);
});
test('read with no path or uid provided fails', async ({ page }) => {
const result = await page.evaluate(async () => {
const puter = (window as any).puter;
try {
await puter.fs.read('');
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
});
expect(result.success).toBe(false);
expect(result.code).toBeTruthy();
});
test('read non-existing file fails', async ({ page }) => {
const result = await page.evaluate(async ({ basePath }) => {
const puter = (window as any).puter;
try {
await puter.fs.read(`${basePath}/i-do-not-exist.txt`);
return { success: true };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { basePath: BASE_PATH });
expect(result.success).toBe(false);
expect(result.code).toBe('subject_does_not_exist');
});
@@ -0,0 +1,253 @@
import { expect } from '@playwright/test';
import { BASE_PATH, test } from './fixtures';
test('write to new directory with default name', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_1`;
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Write file with default name
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const contents = new Blob(['test content 1\n'], { type: 'text/plain' });
const file = new File([contents], 'uploaded_name.txt', { type: 'text/plain' });
const result = await puter.fs.write(`${testPath}/uploaded_name.txt`, file);
return result;
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('uploaded_name.txt');
});
test('write with specified name', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_2`;
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Write file with specified name
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const contents = new Blob(['test content 2\n'], { type: 'text/plain' });
const file = new File([contents], 'uploaded_name.txt', { type: 'text/plain' });
const result = await puter.fs.write(`${testPath}/uploaded_name.txt`, file);
return result;
}, { testPath });
expect(result).toBeTruthy();
});
test('write with overwrite option', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_3`;
const fileName = 'test_overwrite.txt';
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Write initial file
await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['initial content\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
await puter.fs.write(`${testPath}/${fileName}`, file);
}, { testPath, fileName });
// Write with overwrite
const result = await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['updated content\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
const result = await puter.fs.write(`${testPath}/${fileName}`, file, { overwrite: true });
return result;
}, { testPath, fileName });
expect(result).toBeTruthy();
// Verify content was overwritten
const readResult = await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const result = await puter.fs.read(`${testPath}/${fileName}`);
return result.text();
}, { testPath, fileName });
expect(readResult).toBe('updated content\n');
});
test('write to directory using UID', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_4`;
// Create directory and get UID
const dirUID = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
const stat = await puter.fs.stat(testPath);
return stat.uid;
}, { testPath });
// Write file using UID
const result = await page.evaluate(async ({ dirUID }) => {
const puter = (window as any).puter;
const contents = new Blob(['test content with UID\n'], { type: 'text/plain' });
const file = new File([contents], 'uid_test.txt', { type: 'text/plain' });
// Note: puter-js write doesn't directly support UID for destination
// This would require using the internal API
return { uid: dirUID };
}, { dirUID });
expect(result.uid).toBeTruthy();
});
test('write with dedupe name option', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_5`;
const fileName = 'dedupe_test.txt';
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Write initial file
await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['initial\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
await puter.fs.write(`${testPath}/${fileName}`, file);
}, { testPath, fileName });
// Write with dedupeName (without overwrite)
const result = await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['deduped\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
try {
const result = await puter.fs.write(`${testPath}/${fileName}`, file, {
overwrite: false,
dedupeName: true
});
return { success: true, result };
} catch (error: any) {
return { success: false, error: error.message };
}
}, { testPath, fileName });
expect(result.success).toBe(true);
expect(result.result.name).toMatch(/dedupe_test \(\d\)\.txt/);
});
test('write string data', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_6`;
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Write string data
const result = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const result = await puter.fs.write(`${testPath}/string_test.txt`, 'Hello World\n');
return result;
}, { testPath });
expect(result).toBeTruthy();
expect(result.name).toBe('string_test.txt');
// Verify content
const readResult = await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
const result = await puter.fs.read(`${testPath}/string_test.txt`);
return result.text();
}, { testPath });
expect(readResult).toBe('Hello World\n');
});
test('write to file instead of directory should error', async ({ page }) => {
const testPath = `${BASE_PATH}/write_test_7`;
const fileName = 'destination.txt';
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Create a file
await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['initial content\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
await puter.fs.write(`${testPath}/${fileName}`, file);
}, { testPath, fileName });
// Try to write to a file (should error or create a nested file)
const result = await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
try {
const contents = new Blob(['test\n'], { type: 'text/plain' });
const file = new File([contents], 'nested.txt', { type: 'text/plain' });
const result = await puter.fs.write(`${testPath}/${fileName}`, file);
return { success: true, result };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { testPath, fileName });
// Note: puter-js behavior might differ from the API tester
// The exact behavior depends on implementation
expect(result.success !== undefined).toBe(true);
});
test('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';
// Create directory
await page.evaluate(async ({ testPath }) => {
const puter = (window as any).puter;
await puter.fs.mkdir(testPath);
}, { testPath });
// Create initial file
await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
const contents = new Blob(['initial\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
await puter.fs.write(`${testPath}/${fileName}`, file);
}, { testPath, fileName });
// Try to write without overwrite - should create deduped name
const result = await page.evaluate(async ({ testPath, fileName }) => {
const puter = (window as any).puter;
try {
const contents = new Blob(['second\n'], { type: 'text/plain' });
const file = new File([contents], fileName, { type: 'text/plain' });
const result = await puter.fs.write(`${testPath}/${fileName}`, file, { overwrite: false });
return { success: true, result };
} catch (error: any) {
return { success: false, error: error.message, code: error.code };
}
}, { testPath, fileName });
// With overwrite: false, it should create a deduped filename
expect(result.success).toBe(true);
expect(result.result.name).toBe(dedupedFileName);
});
+52
View File
@@ -0,0 +1,52 @@
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);
});
+26 -8
View File
@@ -1,16 +1,34 @@
// testUtils.ts - Puter.js API test utilities (TypeScript)
import type { Puter } from '../../src/puter-js';
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'yaml';
import type { Puter } from '../../src/puter-js/index.js';
// Create and configure a global puter instance from environment variables
// Create and configure a global puter instance from client-config.yaml
// Usage: import { puter } from './testUtils'
// Environment variables: PUTER_AUTH_TOKEN, PUTER_API_ORIGIN, PUTER_ORIGIN
// Configuration is read from tests/client-config.yaml
// Load configuration from YAML file
let config: any;
try {
const configPath = path.join(__dirname, '../client-config.yaml');
config = yaml.parse(fs.readFileSync(configPath, 'utf8'));
} catch (error) {
console.error('Failed to load client-config.yaml:', error);
process.exit(1);
}
// @ts-ignore
const puter: Puter = require('../../src/puter-js/src/index.js').default || globalThis.puter;
globalThis.PUTER_ORIGIN = process.env.PUTER_ORIGIN || 'https://puter.com';
globalThis.PUTER_API_ORIGIN = process.env.PUTER_API_ORIGIN || 'https://api.puter.com';
if (process.env.PUTER_API_ORIGIN) (puter as any).setAPIOrigin(process.env.PUTER_API_ORIGIN);
if (process.env.PUTER_ORIGIN) (puter as any).defaultGUIOrigin = process.env.PUTER_ORIGIN;
if (process.env.PUTER_AUTH_TOKEN) (puter as any).setAuthToken(process.env.PUTER_AUTH_TOKEN);
(globalThis as any).PUTER_ORIGIN = config.frontend_url;
(globalThis as any).PUTER_API_ORIGIN = config.api_url;
(puter as any).setAPIOrigin(config.api_url);
(puter as any).defaultGUIOrigin = config.frontend_url;
if (config.auth_token) {
(puter as any).setAuthToken(config.auth_token);
}
export { puter };
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": ".."
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
-16
View File
@@ -1,16 +0,0 @@
# API Test
## Summary
This script tests the APIs of the Puter backend and `puter-js`.
## Usage
```bash
pip install -r ./tools/api-tester/ci/requirements.txt
./tools/api-tester/ci/run.py
```
## TODO
- [ ] Support macOS.
-187
View File
@@ -1,187 +0,0 @@
#! /usr/bin/env python3
#
# Usage:
# ./tools/api-tester/ci/run.py
import argparse
import time
import sys
import os
import json
import datetime
import urllib
import requests
import yaml
import cxc_toolkit
import cxc_toolkit.exec
class Context:
def __init__(self):
self.ADMIN_PASSWORD = None
self.TOKEN = None
CONTEXT = Context()
def get_token():
# Send HTTP request to server and print response
print("Sending HTTP request to server...")
# Assuming the server runs on localhost:4100 (default Puter port)
server_url = "http://api.puter.localhost:4100/login"
# Prepare login data
login_data = {"username": "admin", "password": CONTEXT.ADMIN_PASSWORD}
# Send POST request using requests library
response = requests.post(
server_url,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Origin": "http://api.puter.localhost:4100",
},
json=login_data,
timeout=30,
)
print(f"Server response status: {response.status_code}")
print(f"Server response body: {response.text}")
response_json = response.json()
print(f"Parsed JSON response: {json.dumps(response_json, indent=2)}")
print(f"Token: {response_json['token']}")
CONTEXT.TOKEN = response_json["token"]
def init_server_config():
server_process = cxc_toolkit.exec.run_background("npm start")
# wait 10s for the server to start
time.sleep(10)
server_process.terminate()
# create the admin user and print its password
def get_admin_password():
# output_bytes, exit_code = cxc_toolkit.exec.run_command(
# "npm start",
# stream_output=False,
# kill_on_output="password for admin",
# )
backend_process = cxc_toolkit.exec.run_background(
"npm start", log_path="/tmp/backend.log"
)
# NB: run_command + kill_on_output may wait indefinitely, use run_background + hard limit instead
time.sleep(10)
backend_process.terminate()
# read the log file
with open("/tmp/backend.log", "r") as f:
lines = f.readlines()
for line in lines:
if "password for admin" in line:
print(f"found password line: ---{line}---")
admin_password = line.split("password for admin is:")[1].strip()
print(f"Extracted admin password: {admin_password}")
CONTEXT.ADMIN_PASSWORD = admin_password
return
if not CONTEXT.ADMIN_PASSWORD:
print("Error: No admin password found")
# print the log file
with open("/tmp/backend.log", "r") as f:
print(f.read())
exit(1)
def update_server_config():
# Load the config file
config_file = f"{os.getcwd()}/volatile/config/config.json"
with open(config_file, "r") as f:
config = json.load(f)
# Ensure services and mountpoint sections exist
if "services" not in config:
config["services"] = {}
if "mountpoint" not in config["services"]:
config["services"]["mountpoint"] = {}
if "mountpoints" not in config["services"]["mountpoint"]:
config["services"]["mountpoint"]["mountpoints"] = {}
# Add the mountpoint configuration
mountpoint_config = {
"/": {"mounter": "puterfs"},
"/admin/tmp": {"mounter": "memoryfs"},
}
# Merge mountpoints (overwrite existing ones)
config["services"]["mountpoint"]["mountpoints"].update(mountpoint_config)
# Write the updated config back
with open(config_file, "w") as f:
json.dump(config, f, indent=2)
def init_api_test():
# Load the example config
example_config_path = f"{os.getcwd()}/tools/api-tester/example_config.yml"
config_path = f"{os.getcwd()}/tools/api-tester/config.yml"
with open(example_config_path, "r") as f:
config = yaml.safe_load(f)
# Update the token
if not CONTEXT.TOKEN:
print("Warning: No token available in CONTEXT")
exit(1)
config["token"] = CONTEXT.TOKEN
config["url"] = "http://api.puter.localhost:4100"
# Write the updated config
with open(config_path, "w") as f:
yaml.dump(config, f, default_flow_style=False, indent=2)
def run():
# =========================================================================
# free the port 4100
# =========================================================================
cxc_toolkit.exec.run_command("fuser -k 4100/tcp", ignore_failure=True)
# =========================================================================
# config server
# =========================================================================
cxc_toolkit.exec.run_command("npm install")
init_server_config()
get_admin_password()
update_server_config()
# =========================================================================
# config client
# =========================================================================
cxc_toolkit.exec.run_background("npm start")
# wait 10s for the server to start
time.sleep(10)
get_token()
init_api_test()
# =========================================================================
# run the test
# =========================================================================
cxc_toolkit.exec.run_command(
"node ./tools/api-tester/apitest.js --unit --stop-on-failure"
)
if __name__ == "__main__":
run()
-8
View File
@@ -1,8 +0,0 @@
url: http://api.puter.localhost:4100/
username: admin
token: ---
mountpoints:
- path: /
provider: puterfs
- path: /admin/tmp
provider: memoryfs
-388
View File
@@ -1,388 +0,0 @@
{
"name": "puter-api-test",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "puter-api-test",
"version": "0.1.0",
"license": "UNLICENSED",
"dependencies": {
"axios": "^1.12.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"yaml": "^2.3.1"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"engines": {
"node": "*"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chai": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
"integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^4.1.2",
"get-func-name": "^2.0.0",
"loupe": "^2.3.1",
"pathval": "^1.1.1",
"type-detect": "^4.0.5"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chai-as-promised": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
"integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
"dependencies": {
"check-error": "^1.0.2"
},
"peerDependencies": {
"chai": ">= 2.1.2 < 5"
}
},
"node_modules/check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
"engines": {
"node": "*"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/deep-eql": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
"dependencies": {
"type-detect": "^4.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/loupe": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
"integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
"dependencies": {
"get-func-name": "^2.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"engines": {
"node": "*"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"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==",
"engines": {
"node": ">=4"
}
},
"node_modules/yaml": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
"engines": {
"node": ">= 14"
}
}
}
}