diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f319c22..54669f6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: CI on: push: @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true jobs: - lint: + checks: runs-on: ubuntu-latest steps: - name: Check out repository @@ -25,9 +25,35 @@ jobs: uses: actions/setup-node@v4 with: node-version: "24" + cache: npm - name: Install dependencies run: npm ci - name: Run lint run: npm run lint + + - name: Run typecheck + run: npm run typecheck + + - name: Run coverage + run: npm run coverage + + - name: Upload coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage + + - name: Upload coverage to Codecov + if: ${{ env.CODECOV_TOKEN != '' }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@v5 + with: + files: ./coverage/lcov.info + flags: unit + name: vitest-coverage + token: ${{ env.CODECOV_TOKEN }} + fail_ci_if_error: false + verbose: true diff --git a/.gitignore b/.gitignore index c2658d7..25fbf5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +coverage/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c13a0b7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,73 @@ +# AGENTS.md + +## Repo Scope + +This repository contains the Agent Control plugin for OpenClaw. It is a TypeScript ESM project that ships source files directly; there is no separate build step in normal development. + +## Local Verification + +Run the full local verification stack before finishing non-trivial changes: + +```bash +npm run lint +npm run typecheck +npm test +``` + +When the change affects tests, coverage, or CI behavior, also run: + +```bash +npm run coverage +``` + +Coverage output is written to `coverage/`, including `coverage/lcov.info` for Codecov-compatible uploads. + +## Testing Conventions + +- Prefer behavioral tests over implementation-detail tests. +- Write test names as concise behavioral summaries. +- Express Given/When/Then structure as code comments inside the test body. +- Make each Given/When/Then comment descriptive. Do not use placeholder comments like `// Given`, `// When`, or `// Then` by themselves. +- Use Vitest for unit and integration-style tests. +- Assert externally visible outcomes first: return values, registered hooks, emitted logs, blocked tool calls, resolved context, and client calls. +- Mock boundary dependencies such as `agent-control`, session/context helpers, and runtime-loading edges when needed, but keep the assertions focused on plugin behavior. +- When adding a new branch in plugin logic, add or update tests in the corresponding `test/*.test.ts` file. + +Examples of the preferred naming style: + +- `it("defaults to warn", () => { ... })` +- `it("blocks the tool call when fail-closed sync fails", async () => { ... })` + +Examples of the preferred comment style: + +- `// Given no logging configuration is provided` +- `// When the effective log level is resolved` +- `// Then warn mode is selected by default` + +## Project Conventions + +- Keep imports ESM-compatible and include the `.ts` suffix for local TypeScript imports, matching the current codebase style. +- This repo uses `oxlint` for linting and `tsc --noEmit` for semantic typechecking. `npm run lint` is not a substitute for `npm run typecheck`. +- Preserve the plugin's quiet default behavior. New logs should fit the existing `logLevel` model: + - `warn`: warnings, errors, and block events + - `info`: important lifecycle events + - `debug`: verbose diagnostics + +## When Changing Config or User-Facing Behavior + +Keep these files aligned when config shape or documented behavior changes: + +- `src/types.ts` +- `openclaw.plugin.json` +- `README.md` +- relevant tests under `test/` + +If a change affects CI expectations or coverage behavior, also update: + +- `.github/workflows/lint.yml` +- `package.json` +- `vitest.config.ts` + +## CI Expectations + +The main GitHub Actions workflow is `.github/workflows/lint.yml`. Changes to scripts or coverage generation should keep local commands and CI steps in sync. diff --git a/README.md b/README.md index 5edbed5..9d7951f 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,14 @@ Then restart the gateway. ```bash npm install +npm run lint +npm run typecheck +npm test +npm run coverage ``` +Coverage reports are written to `coverage/`, including `coverage/lcov.info` for Codecov-compatible uploads. The GitHub Actions workflow will upload that report to Codecov automatically when a `CODECOV_TOKEN` secret is configured for the repository. + 3. Link it into your OpenClaw config from your OpenClaw checkout: ```bash @@ -66,12 +72,16 @@ openclaw config set plugins.entries.agent-control-openclaw-plugin.config.timeout openclaw config set plugins.entries.agent-control-openclaw-plugin.config.failClosed false --strict-json # Optional settings +openclaw config set plugins.entries.agent-control-openclaw-plugin.config.logLevel "info" +openclaw config set plugins.entries.agent-control-openclaw-plugin.config.logLevel "debug" openclaw config set plugins.entries.agent-control-openclaw-plugin.config.agentId "00000000-0000-4000-8000-000000000000" openclaw config set plugins.entries.agent-control-openclaw-plugin.config.agentVersion "2026.3.3" openclaw config set plugins.entries.agent-control-openclaw-plugin.config.userAgent "agent-control-plugin/0.1" # Remove optional keys openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.apiKey +openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.logLevel +openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.debug openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.agentId openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.agentVersion openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.userAgent @@ -79,3 +89,12 @@ openclaw config unset plugins.entries.agent-control-openclaw-plugin.config.userA # Uninstall plugin link/install record from OpenClaw config openclaw plugins uninstall agent-control-openclaw-plugin --force ``` + +By default the plugin stays quiet and only emits warnings, errors, and tool block events. + +Set `config.logLevel` to: + +- `info` for one-line lifecycle logs such as client init, warmup, and agent syncs +- `debug` for verbose startup, sync, and evaluation diagnostics + +The older `config.debug` flag is still accepted as a deprecated alias for `logLevel=debug`. diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 72adbf5..ecadb48 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -33,6 +33,13 @@ }, "failClosed": { "type": "boolean" + }, + "logLevel": { + "type": "string", + "enum": ["warn", "info", "debug"] + }, + "debug": { + "type": "boolean" } } }, @@ -56,6 +63,14 @@ "failClosed": { "label": "Fail Closed", "help": "If true, block tool invocations when Agent Control is unavailable." + }, + "logLevel": { + "label": "Log Level", + "help": "Controls plugin verbosity: warn logs only warnings, errors, and block events; info adds high-level lifecycle logs; debug adds verbose diagnostics." + }, + "debug": { + "label": "Debug Logging", + "help": "Deprecated alias for logLevel=debug." } } } diff --git a/package-lock.json b/package-lock.json index d9ba273..43db100 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,11 @@ }, "devDependencies": { "@semantic-release/git": "^10.0.1", + "@types/node": "^24.5.2", + "@vitest/coverage-v8": "^4.0.18", "oxlint": "^0.15.0", "semantic-release": "^25.0.3", + "typescript": "^5.9.2", "vitest": "^4.0.18" } }, @@ -83,6 +86,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -93,6 +106,46 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -104,6 +157,40 @@ "node": ">=0.1.90" } }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -494,6 +581,16 @@ "node": ">=18" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -501,6 +598,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -658,6 +783,16 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@oxlint/darwin-arm64": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/@oxlint/darwin-arm64/-/darwin-arm64-0.15.0.tgz", @@ -807,24 +942,10 @@ "node": ">=12" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", "cpu": [ "arm64" ], @@ -833,12 +954,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", "cpu": [ "arm64" ], @@ -847,12 +971,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", "cpu": [ "x64" ], @@ -861,26 +988,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", "cpu": [ "x64" ], @@ -889,26 +1005,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", "cpu": [ "arm" ], @@ -917,26 +1022,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", "cpu": [ "arm64" ], @@ -945,54 +1039,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", "cpu": [ "ppc64" ], @@ -1001,40 +1073,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", "cpu": [ "s390x" ], @@ -1043,12 +1090,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", "cpu": [ "x64" ], @@ -1057,12 +1107,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", "cpu": [ "x64" ], @@ -1071,26 +1124,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", "cpu": [ "arm64" ], @@ -1099,40 +1141,49 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", "cpu": [ "x64" ], @@ -1141,21 +1192,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", @@ -1640,6 +1687,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1665,6 +1723,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -1672,41 +1740,72 @@ "dev": true, "license": "MIT" }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", + "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.0", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.0", + "vitest": "4.1.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1718,9 +1817,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1731,13 +1830,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -1745,13 +1844,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1760,9 +1860,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -1770,13 +1870,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1902,6 +2003,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -2289,6 +2409,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2395,6 +2522,16 @@ "node": ">=4.0.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2595,9 +2732,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -3015,6 +3152,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3269,6 +3413,45 @@ "node": "^18.17 || >=20.6.1" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -3342,6 +3525,267 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3462,6 +3906,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/make-asynchronous": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", @@ -3493,6 +3949,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/marked": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", @@ -6278,49 +6750,38 @@ "node": ">=8" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" } }, "node_modules/safe-buffer": { @@ -6642,9 +7103,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -6976,6 +7437,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -7002,7 +7471,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/uglify-js": { + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", @@ -7026,6 +7509,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-emoji-modifier-base": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", @@ -7111,24 +7601,23 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7137,14 +7626,15 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -7153,13 +7643,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -7185,489 +7678,32 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -7683,12 +7719,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -7717,6 +7754,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index e94d10d..8ffd495 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "scripts": { "lint": "oxlint .", "lint:fix": "oxlint . --fix", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "coverage": "vitest run --coverage", "release": "semantic-release" }, "dependencies": { @@ -37,9 +40,12 @@ "jiti": "^2.6.1" }, "devDependencies": { + "@vitest/coverage-v8": "^4.0.18", "@semantic-release/git": "^10.0.1", + "@types/node": "^24.5.2", "oxlint": "^0.15.0", "semantic-release": "^25.0.3", + "typescript": "^5.9.2", "vitest": "^4.0.18" }, "openclaw": { diff --git a/src/agent-control-plugin.ts b/src/agent-control-plugin.ts index 9894c7a..802f989 100644 --- a/src/agent-control-plugin.ts +++ b/src/agent-control-plugin.ts @@ -1,5 +1,6 @@ import { AgentControlClient } from "agent-control"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import { createPluginLogger, resolveLogLevel } from "./logging.ts"; import { resolveStepsForContext } from "./tool-catalog.ts"; import { buildEvaluationContext } from "./session-context.ts"; import { @@ -17,8 +18,8 @@ import { import type { AgentControlPluginConfig, AgentState } from "./types.ts"; function collectDenyControlNames(response: { - matches?: Array<{ action?: string; controlName?: string }>; - errors?: Array<{ action?: string; controlName?: string }>; + matches?: Array<{ action?: string; controlName?: string }> | null; + errors?: Array<{ action?: string; controlName?: string }> | null; }): string[] { const names: string[] = []; for (const match of [...(response.matches ?? []), ...(response.errors ?? [])]) { @@ -35,8 +36,8 @@ function collectDenyControlNames(response: { function buildBlockReason(response: { reason?: string | null; - matches?: Array<{ action?: string; controlName?: string }>; - errors?: Array<{ action?: string; controlName?: string }>; + matches?: Array<{ action?: string; controlName?: string }> | null; + errors?: Array<{ action?: string; controlName?: string }> | null; }): string { const denyControls = collectDenyControlNames(response); if (denyControls.length > 0) { @@ -63,10 +64,11 @@ export default function register(api: OpenClawPluginApi) { if (cfg.enabled === false) { return; } + const logger = createPluginLogger(api.logger, resolveLogLevel(cfg)); const serverUrl = asString(cfg.serverUrl) ?? asString(process.env.AGENT_CONTROL_SERVER_URL); if (!serverUrl) { - api.logger.warn( + logger.warn( "agent-control: disabled because serverUrl is not configured (plugins.entries.agent-control-openclaw-plugin.config.serverUrl)", ); return; @@ -74,7 +76,7 @@ export default function register(api: OpenClawPluginApi) { const configuredAgentId = asString(cfg.agentId); if (configuredAgentId && !isUuid(configuredAgentId)) { - api.logger.warn(`agent-control: configured agentId is not a UUID: ${configuredAgentId}`); + logger.warn(`agent-control: configured agentId is not a UUID: ${configuredAgentId}`); } const hasConfiguredAgentId = configuredAgentId ? isUuid(configuredAgentId) : false; @@ -93,7 +95,7 @@ export default function register(api: OpenClawPluginApi) { timeoutMs: clientTimeoutMs, userAgent: asString(cfg.userAgent) ?? "openclaw-agent-control-plugin/0.1", }); - api.logger.info( + logger.info( `agent-control: client_init duration_sec=${secondsSince(clientInitStartedAt)} timeout_ms=${clientTimeoutMs ?? "default"} server_url=${serverUrl}`, ); @@ -130,23 +132,24 @@ export default function register(api: OpenClawPluginApi) { const warmupStartedAt = process.hrtime.bigint(); gatewayWarmupStatus = "running"; - api.logger.info(`agent-control: gateway_boot_warmup started agent=${BOOT_WARMUP_AGENT_ID}`); + logger.info(`agent-control: gateway_boot_warmup started agent=${BOOT_WARMUP_AGENT_ID}`); // Warm the exact resolver path used during tool evaluation so the gateway // process retains the expensive module graph in memory after startup. gatewayWarmupPromise = resolveStepsForContext({ api, + logger, sourceAgentId: BOOT_WARMUP_AGENT_ID, }) .then((steps) => { gatewayWarmupStatus = "done"; - api.logger.info( + logger.info( `agent-control: gateway_boot_warmup done duration_sec=${secondsSince(warmupStartedAt)} agent=${BOOT_WARMUP_AGENT_ID} steps=${steps.length}`, ); }) .catch((err) => { gatewayWarmupStatus = "failed"; - api.logger.warn( + logger.warn( `agent-control: gateway_boot_warmup failed duration_sec=${secondsSince(warmupStartedAt)} agent=${BOOT_WARMUP_AGENT_ID} error=${String(err)}`, ); }); @@ -165,6 +168,7 @@ export default function register(api: OpenClawPluginApi) { const currentHash = state.stepsHash; const promise = (async () => { + const syncStartedAt = process.hrtime.bigint(); await client.agents.init({ agent: { agentName: state.agentName, @@ -178,6 +182,9 @@ export default function register(api: OpenClawPluginApi) { }, steps: state.steps, }); + logger.info( + `agent-control: sync_agent duration_sec=${secondsSince(syncStartedAt)} agent=${state.sourceAgentId} step_count=${state.steps.length}`, + ); state.lastSyncedStepsHash = currentHash; })().finally(() => { state.syncPromise = null; @@ -201,18 +208,18 @@ export default function register(api: OpenClawPluginApi) { const sourceAgentId = resolveSourceAgentId(ctx.agentId); const state = getOrCreateState(sourceAgentId); const argsForLog = formatToolArgsForLog(event.params); - api.logger.info( + logger.debug( `agent-control: before_tool_call entered agent=${sourceAgentId} tool=${event.toolName} args=${argsForLog}`, ); try { if (gatewayWarmupStatus === "running" && gatewayWarmupPromise) { const warmupWaitStartedAt = process.hrtime.bigint(); - api.logger.info( + logger.debug( `agent-control: before_tool_call waiting_for_gateway_boot_warmup=true agent=${sourceAgentId} tool=${event.toolName}`, ); await gatewayWarmupPromise; - api.logger.info( + logger.debug( `agent-control: before_tool_call phase=wait_boot_warmup duration_sec=${secondsSince(warmupWaitStartedAt)} agent=${sourceAgentId} tool=${event.toolName} warmup_status=${gatewayWarmupStatus}`, ); } @@ -221,6 +228,7 @@ export default function register(api: OpenClawPluginApi) { const resolveStepsStartedAt = process.hrtime.bigint(); const nextSteps = await resolveStepsForContext({ api, + logger, sourceAgentId, sessionKey: ctx.sessionKey, sessionId: ctx.sessionId, @@ -231,20 +239,23 @@ export default function register(api: OpenClawPluginApi) { state.steps = nextSteps; state.stepsHash = nextStepsHash; } - api.logger.info( + logger.debug( `agent-control: before_tool_call phase=resolve_steps duration_sec=${secondsSince(resolveStepsStartedAt)} agent=${sourceAgentId} tool=${event.toolName} steps=${nextSteps.length}`, ); const syncStartedAt = process.hrtime.bigint(); await syncAgent(state); - api.logger.info( + logger.debug( `agent-control: before_tool_call phase=sync_agent duration_sec=${secondsSince(syncStartedAt)} agent=${sourceAgentId} tool=${event.toolName} step_count=${state.steps.length}`, ); } catch (err) { - api.logger.warn( + logger.warn( `agent-control: unable to sync agent=${sourceAgentId} before tool evaluation: ${String(err)}`, ); if (failClosed) { + logger.block( + `agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=agent_sync_failed fail_closed=true`, + ); return { block: true, blockReason: USER_BLOCK_MESSAGE, @@ -274,11 +285,11 @@ export default function register(api: OpenClawPluginApi) { configuredAgentId, configuredAgentVersion, }); - api.logger.info( + logger.debug( `agent-control: before_tool_call phase=build_context duration_sec=${secondsSince(contextBuildStartedAt)} agent=${sourceAgentId} tool=${event.toolName}`, ); - api.logger.info( + logger.debug( `agent-control: before_tool_call evaluated agent=${sourceAgentId} tool=${event.toolName} args=${argsForLog} context=${JSON.stringify(context, null, 2)}`, ); @@ -295,17 +306,15 @@ export default function register(api: OpenClawPluginApi) { }, }, }); - api.logger.info( + logger.debug( `agent-control: before_tool_call phase=evaluate duration_sec=${secondsSince(evaluateStartedAt)} agent=${sourceAgentId} tool=${event.toolName} safe=${evaluation.isSafe}`, ); if (evaluation.isSafe) { - api.logger.info("safe !"); return; } - api.logger.info("unsafe !"); - api.logger.warn( + logger.block( `agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=${buildBlockReason(evaluation)}`, ); return { @@ -313,10 +322,13 @@ export default function register(api: OpenClawPluginApi) { blockReason: USER_BLOCK_MESSAGE, }; } catch (err) { - api.logger.warn( + logger.warn( `agent-control: evaluation failed for agent=${sourceAgentId} tool=${event.toolName}: ${String(err)}`, ); if (failClosed) { + logger.block( + `agent-control: blocked tool=${event.toolName} agent=${sourceAgentId} reason=evaluation_failed fail_closed=true`, + ); return { block: true, blockReason: USER_BLOCK_MESSAGE, @@ -325,7 +337,7 @@ export default function register(api: OpenClawPluginApi) { return; } } finally { - api.logger.info( + logger.debug( `agent-control: before_tool_call duration_sec=${secondsSince(beforeToolCallStartedAt)} agent=${sourceAgentId} tool=${event.toolName}`, ); } diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..8e9ed8d --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,42 @@ +import { asString } from "./shared.ts"; +import type { AgentControlPluginConfig, LogLevel, LoggerLike, PluginLogger } from "./types.ts"; + +const LOG_LEVELS: LogLevel[] = ["warn", "info", "debug"]; + +function isLogLevel(value: string): value is LogLevel { + return LOG_LEVELS.includes(value as LogLevel); +} + +export function resolveLogLevel(cfg: AgentControlPluginConfig): LogLevel { + const configuredLevel = asString(cfg.logLevel)?.toLowerCase(); + if (configuredLevel && isLogLevel(configuredLevel)) { + return configuredLevel; + } + if (cfg.debug === true) { + return "debug"; + } + return "warn"; +} + +export function createPluginLogger(logger: LoggerLike, logLevel: LogLevel): PluginLogger { + const infoEnabled = logLevel === "info" || logLevel === "debug"; + const debugEnabled = logLevel === "debug"; + return { + info(message: string) { + if (infoEnabled) { + logger.info(message); + } + }, + debug(message: string) { + if (debugEnabled) { + logger.info(message); + } + }, + warn(message: string) { + logger.warn(message); + }, + block(message: string) { + logger.warn(message); + }, + }; +} diff --git a/src/session-store.ts b/src/session-store.ts index 46b9259..0ba3023 100644 --- a/src/session-store.ts +++ b/src/session-store.ts @@ -148,11 +148,13 @@ export async function resolveSessionIdentity( const sessionCfg = isRecord(cfg.session) ? cfg.session : undefined; const storePath = internals.resolveStorePath(asString(sessionCfg?.store)); const store = internals.loadSessionStore(storePath); - const entry = - (isRecord(store[normalizedKey]) ? store[normalizedKey] : undefined) ?? - (isRecord(store[resolveBaseSessionKey(normalizedKey)]) - ? store[resolveBaseSessionKey(normalizedKey)] - : undefined); + const directEntry = store[normalizedKey]; + const baseEntry = store[resolveBaseSessionKey(normalizedKey)]; + const entry: Record | undefined = isRecord(directEntry) + ? directEntry + : isRecord(baseEntry) + ? baseEntry + : undefined; const data = entry ? readSessionIdentityFromEntry(entry) : unknownSessionIdentity(); setSessionMetadataCache(normalizedKey, data); return data; diff --git a/src/tool-catalog.ts b/src/tool-catalog.ts index 66f7fe3..c961774 100644 --- a/src/tool-catalog.ts +++ b/src/tool-catalog.ts @@ -2,7 +2,13 @@ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import type { AgentControlStep, LoggerLike, ResolveStepsForContextParams, ToolCatalogBundleBuildInfo, ToolCatalogInternals } from "./types.ts"; +import type { + AgentControlStep, + PluginLogger, + ResolveStepsForContextParams, + ToolCatalogBundleBuildInfo, + ToolCatalogInternals, +} from "./types.ts"; import { asString, sanitizeToolCatalogConfig, secondsSince, toJsonRecord } from "./shared.ts"; import { getResolvedOpenClawRootDir, @@ -51,14 +57,14 @@ function hasToolCatalogBundleSources(openClawRoot: string): boolean { } async function importToolCatalogBundleModule( - logger: LoggerLike, + logger: PluginLogger, buildInfo: ToolCatalogBundleBuildInfo, ): Promise> { const importStartedAt = process.hrtime.bigint(); const bundleMtime = safeStatMtimeMs(buildInfo.bundlePath) ?? Date.now(); const bundleUrl = `${pathToFileURL(buildInfo.bundlePath).href}?mtime=${bundleMtime}`; const imported = (await import(bundleUrl)) as Record; - logger.info( + logger.debug( `agent-control: bundle_import_done duration_sec=${secondsSince(importStartedAt)} cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`, ); return imported; @@ -85,18 +91,18 @@ function resolveToolCatalogInternalsFromModules(params: { } async function ensureToolCatalogBundle( - logger: LoggerLike, + logger: PluginLogger, buildInfo: ToolCatalogBundleBuildInfo, ): Promise { if (fs.existsSync(buildInfo.bundlePath)) { - logger.info( + logger.debug( `agent-control: bundle_cache_hit cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`, ); return; } const esbuildStartedAt = process.hrtime.bigint(); - logger.info( + logger.debug( `agent-control: bundle_build_started cache_key=${buildInfo.cacheKey} openclaw_root=${buildInfo.openClawRoot}`, ); @@ -155,13 +161,13 @@ async function ensureToolCatalogBundle( )}\n`, "utf8", ); - logger.info( + logger.debug( `agent-control: bundle_build_done duration_sec=${secondsSince(esbuildStartedAt)} cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`, ); } async function loadToolCatalogInternalsFromGeneratedBundle( - logger: LoggerLike, + logger: PluginLogger, openClawRoot: string, ): Promise { if (!hasToolCatalogBundleSources(openClawRoot)) { @@ -194,7 +200,7 @@ async function loadToolCatalogInternalsFromGeneratedBundle( } } -async function loadToolCatalogInternals(logger: LoggerLike): Promise { +async function loadToolCatalogInternals(logger: PluginLogger): Promise { if (toolCatalogInternalsPromise) { return toolCatalogInternalsPromise; } @@ -212,7 +218,9 @@ async function loadToolCatalogInternals(logger: LoggerLike): Promise { const resolveStartedAt = process.hrtime.bigint(); const internalsStartedAt = process.hrtime.bigint(); - const internals = await loadToolCatalogInternals(params.api.logger); + const internals = await loadToolCatalogInternals(params.logger); const internalsDurationSec = secondsSince(internalsStartedAt); const createToolsStartedAt = process.hrtime.bigint(); @@ -318,7 +326,7 @@ export async function resolveStepsForContext( ); const adaptDurationSec = secondsSince(adaptStartedAt); - params.api.logger.info( + params.logger.debug( `agent-control: resolve_steps duration_sec=${secondsSince(resolveStartedAt)} agent=${params.sourceAgentId} internals_sec=${internalsDurationSec} create_tools_sec=${createToolsDurationSec} adapt_sec=${adaptDurationSec} tools=${tools.length} steps=${steps.length}`, ); diff --git a/src/types.ts b/src/types.ts index f81d150..dd6764a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +export type LogLevel = "warn" | "info" | "debug"; + export type AgentControlPluginConfig = { enabled?: boolean; serverUrl?: string; @@ -10,6 +12,8 @@ export type AgentControlPluginConfig = { timeoutMs?: number; userAgent?: string; failClosed?: boolean; + logLevel?: LogLevel; + debug?: boolean; }; export type AgentControlStep = { @@ -80,6 +84,13 @@ export type SessionMetadataCacheEntry = { export type LoggerLike = Pick; +export type PluginLogger = { + info: (message: string) => void; + debug: (message: string) => void; + warn: (message: string) => void; + block: (message: string) => void; +}; + export type ToolCatalogBundleBuildInfo = { bundlePath: string; cacheDir: string; @@ -91,6 +102,7 @@ export type ToolCatalogBundleBuildInfo = { export type ResolveStepsForContextParams = { api: OpenClawPluginApi; + logger: PluginLogger; sourceAgentId: string; sessionKey?: string; sessionId?: string; diff --git a/test/agent-control-plugin.test.ts b/test/agent-control-plugin.test.ts new file mode 100644 index 0000000..92022f3 --- /dev/null +++ b/test/agent-control-plugin.test.ts @@ -0,0 +1,419 @@ +import type { + OpenClawBeforeToolCallContext, + OpenClawBeforeToolCallEvent, + OpenClawPluginApi, +} from "openclaw/plugin-sdk/core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { USER_BLOCK_MESSAGE } from "../src/shared.ts"; + +const { + clientMocks, + resolveStepsForContextMock, + buildEvaluationContextMock, +} = vi.hoisted(() => ({ + clientMocks: { + init: vi.fn(), + agentsInit: vi.fn(), + evaluationEvaluate: vi.fn(), + }, + resolveStepsForContextMock: vi.fn(), + buildEvaluationContextMock: vi.fn(), +})); + +vi.mock("agent-control", () => ({ + AgentControlClient: class MockAgentControlClient { + init = clientMocks.init; + agents = { + init: clientMocks.agentsInit, + }; + evaluation = { + evaluate: clientMocks.evaluationEvaluate, + }; + }, +})); + +vi.mock("../src/tool-catalog.ts", () => ({ + resolveStepsForContext: resolveStepsForContextMock, +})); + +vi.mock("../src/session-context.ts", () => ({ + buildEvaluationContext: buildEvaluationContextMock, +})); + +import register from "../src/agent-control-plugin.ts"; + +const VALID_AGENT_ID = "00000000-0000-4000-8000-000000000000"; + +type MockApi = { + api: OpenClawPluginApi; + handlers: Map unknown>; + info: ReturnType; + warn: ReturnType; +}; + +function createDeferred() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +function createMockApi(pluginConfig: Record): MockApi { + const handlers = new Map unknown>(); + const info = vi.fn(); + const warn = vi.fn(); + + const api: OpenClawPluginApi = { + id: "agent-control-openclaw-plugin", + version: "test-version", + config: {}, + pluginConfig, + logger: { + info, + warn, + }, + on(event, handler) { + handlers.set(event, handler as (...args: any[]) => unknown); + }, + }; + + return { api, handlers, info, warn }; +} + +async function runBeforeToolCall( + api: MockApi, + event: Partial = {}, + ctx: Partial = {}, +): Promise { + const handler = api.handlers.get("before_tool_call"); + if (!handler) { + throw new Error("before_tool_call handler was not registered"); + } + return handler( + { + toolName: "shell", + params: { cmd: "echo hi" }, + runId: "run-1", + toolCallId: "call-1", + ...event, + }, + ctx, + ); +} + +async function runGatewayStart(api: MockApi): Promise { + const handler = api.handlers.get("gateway_start"); + if (!handler) { + throw new Error("gateway_start handler was not registered"); + } + await handler(); +} + +beforeEach(() => { + clientMocks.init.mockReset(); + clientMocks.agentsInit.mockReset().mockResolvedValue(undefined); + clientMocks.evaluationEvaluate.mockReset().mockResolvedValue({ isSafe: true }); + resolveStepsForContextMock.mockReset().mockResolvedValue([{ type: "tool", name: "shell" }]); + buildEvaluationContextMock.mockReset().mockResolvedValue({ channelType: "unknown" }); +}); + +describe("agent-control plugin logging and blocking", () => { + it("skips initialization when the plugin is disabled", () => { + // Given plugin configuration with the plugin explicitly disabled + const api = createMockApi({ + enabled: false, + serverUrl: "http://localhost:8000", + }); + + // When the plugin is registered with the OpenClaw API + register(api.api); + + // Then no client initialization or hook registration occurs + expect(clientMocks.init).not.toHaveBeenCalled(); + expect(api.handlers.size).toBe(0); + }); + + it("warns and skips hook registration when no server URL is configured", () => { + // Given plugin configuration without an Agent Control server URL + const api = createMockApi({}); + + // When the plugin is registered + register(api.api); + + // Then registration is skipped and a warning is emitted + expect(clientMocks.init).not.toHaveBeenCalled(); + expect(api.handlers.size).toBe(0); + expect(api.warn).toHaveBeenCalledWith( + expect.stringContaining("disabled because serverUrl is not configured"), + ); + }); + + it("warns when the configured agent ID is not a UUID", () => { + // Given plugin configuration with an invalid configured agent ID + const api = createMockApi({ + serverUrl: "http://localhost:8000", + agentId: "not-a-uuid", + }); + + // When the plugin is registered + register(api.api); + + // Then a UUID validation warning is emitted + expect(api.warn).toHaveBeenCalledWith( + "agent-control: configured agentId is not a UUID: not-a-uuid", + ); + }); + + it("only logs the block event in warn mode for unsafe evaluations", async () => { + // Given warn-level logging and an unsafe policy evaluation response + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + + clientMocks.evaluationEvaluate.mockResolvedValueOnce({ + isSafe: false, + reason: "denied by policy", + }); + + // When the plugin evaluates a tool call + register(api.api); + const result = await runBeforeToolCall(api); + + // Then the tool call is blocked and only the block event is logged + expect(result).toEqual({ + block: true, + blockReason: USER_BLOCK_MESSAGE, + }); + expect(api.info).not.toHaveBeenCalled(); + expect(api.warn).toHaveBeenCalledTimes(1); + expect(api.warn.mock.calls[0]?.[0]).toContain("blocked tool=shell"); + expect(clientMocks.agentsInit).toHaveBeenCalledOnce(); + expect(clientMocks.evaluationEvaluate).toHaveBeenCalledOnce(); + }); + + it("emits lifecycle logs without debug traces in info mode", async () => { + // Given info-level logging for a plugin that can warm up and evaluate tools + const api = createMockApi({ + serverUrl: "http://localhost:8000", + logLevel: "info", + }); + + // When gateway warmup and one tool evaluation are executed + register(api.api); + await runGatewayStart(api); + await runBeforeToolCall(api); + + // Then lifecycle logs are emitted without low-level debug traces + const messages = api.info.mock.calls.map(([message]) => String(message)); + expect(messages.some((message) => message.includes("client_init"))).toBe(true); + expect(messages.some((message) => message.includes("gateway_boot_warmup started"))).toBe(true); + expect(messages.some((message) => message.includes("gateway_boot_warmup done"))).toBe(true); + expect(messages.some((message) => message.includes("sync_agent"))).toBe(true); + expect(messages.some((message) => message.includes("before_tool_call entered"))).toBe(false); + expect(messages.some((message) => message.includes("evaluated agent="))).toBe(false); + }); + + it("emits verbose traces when the deprecated debug flag is enabled", async () => { + // Given the deprecated debug flag enabled in plugin configuration + const api = createMockApi({ + serverUrl: "http://localhost:8000", + debug: true, + }); + + // When the plugin evaluates a tool call + register(api.api); + await runBeforeToolCall(api); + + // Then verbose debug trace messages are emitted + const messages = api.info.mock.calls.map(([message]) => String(message)); + expect(messages.some((message) => message.includes("before_tool_call entered"))).toBe(true); + expect(messages.some((message) => message.includes("phase=evaluate"))).toBe(true); + }); + + it("blocks the tool call before evaluation when fail-closed sync fails", async () => { + // Given fail-closed mode and a step-resolution failure during sync + const api = createMockApi({ + serverUrl: "http://localhost:8000", + failClosed: true, + }); + + resolveStepsForContextMock.mockRejectedValueOnce(new Error("resolver exploded")); + + // When the plugin attempts to evaluate a tool call + register(api.api); + const result = await runBeforeToolCall(api); + + // Then the tool call is blocked before evaluation and failure warnings are logged + expect(result).toEqual({ + block: true, + blockReason: USER_BLOCK_MESSAGE, + }); + expect(buildEvaluationContextMock).not.toHaveBeenCalled(); + expect(clientMocks.evaluationEvaluate).not.toHaveBeenCalled(); + expect(api.warn.mock.calls.map(([message]) => String(message))).toEqual( + expect.arrayContaining([ + expect.stringContaining("unable to sync"), + expect.stringContaining("blocked tool=shell agent=default reason=agent_sync_failed fail_closed=true"), + ]), + ); + }); + + it("uses the base agent name when a fixed configured agent ID is present", async () => { + // Given a fixed configured agent ID and a base agent name + const api = createMockApi({ + serverUrl: "http://localhost:8000", + agentId: VALID_AGENT_ID, + agentName: "base-agent", + }); + + // When a source agent evaluates a tool call + register(api.api); + await runBeforeToolCall(api, {}, { agentId: "worker-1" }); + + // Then Agent Control receives the base agent name without a source suffix + expect(clientMocks.agentsInit).toHaveBeenCalledWith( + expect.objectContaining({ + agent: expect.objectContaining({ + agentName: "base-agent", + agentMetadata: expect.objectContaining({ + openclawConfiguredAgentId: VALID_AGENT_ID, + }), + }), + }), + ); + }); + + it("appends the source agent ID when no configured agent ID is present", async () => { + // Given a base agent name without a fixed configured agent ID + const api = createMockApi({ + serverUrl: "http://localhost:8000", + agentName: "base-agent", + }); + + // When a source agent evaluates a tool call + register(api.api); + await runBeforeToolCall(api, {}, { agentId: "worker-1" }); + + // Then Agent Control receives the base agent name with the source suffix appended + expect(clientMocks.agentsInit).toHaveBeenCalledWith( + expect.objectContaining({ + agent: expect.objectContaining({ + agentName: "base-agent:worker-1", + }), + }), + ); + }); + + it("reuses warmup work across repeated gateway_start events", async () => { + // Given a plugin instance that has already started warmup once + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + + // When gateway_start is fired twice + register(api.api); + await runGatewayStart(api); + await runGatewayStart(api); + + // Then warmup work is reused and step resolution only runs once + expect(resolveStepsForContextMock).toHaveBeenCalledTimes(1); + expect(resolveStepsForContextMock).toHaveBeenCalledWith( + expect.objectContaining({ + sourceAgentId: "main", + }), + ); + }); + + it("deduplicates concurrent syncs for the same source agent", async () => { + // Given two concurrent tool calls sharing the same source agent and sync promise + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + const syncDeferred = createDeferred(); + clientMocks.agentsInit.mockImplementation(() => syncDeferred.promise); + + // When both tool calls are started before the initial sync completes + register(api.api); + + const first = runBeforeToolCall(api); + const second = runBeforeToolCall(api); + await Promise.resolve(); + await Promise.resolve(); + + expect(clientMocks.agentsInit).toHaveBeenCalledTimes(1); + + syncDeferred.resolve(undefined); + await Promise.all([first, second]); + + // Then only one sync starts and both tool calls eventually evaluate + expect(clientMocks.evaluationEvaluate).toHaveBeenCalledTimes(2); + }); + + it("skips resyncing when the step catalog has not changed", async () => { + // Given a source agent whose step catalog is unchanged across two tool calls + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + + // When the plugin evaluates two tool calls back to back + register(api.api); + await runBeforeToolCall(api); + await runBeforeToolCall(api); + + // Then the agent is only synced once while both evaluations still run + expect(clientMocks.agentsInit).toHaveBeenCalledTimes(1); + expect(clientMocks.evaluationEvaluate).toHaveBeenCalledTimes(2); + }); + + it("deduplicates deny controls in the block reason", async () => { + // Given an unsafe evaluation response with duplicate deny controls + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + + clientMocks.evaluationEvaluate.mockResolvedValueOnce({ + isSafe: false, + matches: [ + { action: "deny", controlName: "alpha" }, + { action: "deny", controlName: "alpha" }, + { action: "deny", controlName: "beta" }, + ], + errors: null, + }); + + // When the tool call is evaluated and blocked + register(api.api); + await runBeforeToolCall(api); + + // Then the logged block reason lists each control name only once + const message = String(api.warn.mock.calls[0]?.[0]); + expect(message).toContain("alpha, beta"); + expect(message).not.toContain("alpha, alpha"); + }); + + it("logs the generic block reason when no policy details are returned", async () => { + // Given an unsafe evaluation response with no policy reason or deny controls + const api = createMockApi({ + serverUrl: "http://localhost:8000", + }); + + clientMocks.evaluationEvaluate.mockResolvedValueOnce({ + isSafe: false, + reason: "", + matches: null, + errors: null, + }); + + // When the tool call is evaluated and blocked + register(api.api); + await runBeforeToolCall(api); + + // Then the generic policy block reason is logged + expect(api.warn).toHaveBeenCalledWith( + expect.stringContaining("reason=[agent-control] blocked by policy evaluation"), + ); + }); +}); diff --git a/test/logging.test.ts b/test/logging.test.ts new file mode 100644 index 0000000..8d5362e --- /dev/null +++ b/test/logging.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it, vi } from "vitest"; +import { createPluginLogger, resolveLogLevel } from "../src/logging.ts"; + +describe("resolveLogLevel", () => { + it("defaults to warn", () => { + // Given no logging configuration is provided + const config = {}; + + // When the effective log level is resolved + const level = resolveLogLevel(config); + + // Then warn mode is selected by default + expect(level).toBe("warn"); + }); + + it("uses an explicit configured level", () => { + // Given explicit info and debug log level configurations + const infoConfig = { logLevel: "info" } as const; + const debugConfig = { logLevel: "debug" } as const; + + // When each configuration is resolved + const infoLevel = resolveLogLevel(infoConfig); + const debugLevel = resolveLogLevel(debugConfig); + + // Then the configured levels are preserved + expect(infoLevel).toBe("info"); + expect(debugLevel).toBe("debug"); + }); + + it("prefers logLevel over the deprecated debug flag", () => { + // Given both a logLevel value and the deprecated debug flag + const config = { logLevel: "warn", debug: true } as const; + + // When the effective log level is resolved + const level = resolveLogLevel(config); + + // Then the explicit logLevel takes precedence + expect(level).toBe("warn"); + }); + + it("falls back to debug for deprecated compatibility", () => { + // Given an invalid logLevel alongside debug compatibility mode + const config = { logLevel: "verbose" as never, debug: true }; + + // When the effective log level is resolved + const level = resolveLogLevel(config); + + // Then debug mode is used as the compatibility fallback + expect(level).toBe("debug"); + }); +}); + +describe("createPluginLogger", () => { + it("suppresses info and debug output in warn mode", () => { + // Given a logger configured for warn-only output + const info = vi.fn(); + const warn = vi.fn(); + const logger = createPluginLogger({ info, warn }, "warn"); + + // When info, debug, warn, and block messages are emitted + logger.info("info"); + logger.debug("debug"); + logger.warn("warn"); + logger.block("block"); + + // Then only warning-class messages are forwarded + expect(info).not.toHaveBeenCalled(); + expect(warn.mock.calls).toEqual([["warn"], ["block"]]); + }); + + it("emits info but not debug output in info mode", () => { + // Given a logger configured for info-level output + const info = vi.fn(); + const warn = vi.fn(); + const logger = createPluginLogger({ info, warn }, "info"); + + // When info and debug messages are emitted + logger.info("info"); + logger.debug("debug"); + + // Then only the info message is forwarded + expect(info.mock.calls).toEqual([["info"]]); + expect(warn).not.toHaveBeenCalled(); + }); + + it("emits info and debug output in debug mode", () => { + // Given a logger configured for debug-level output + const info = vi.fn(); + const warn = vi.fn(); + const logger = createPluginLogger({ info, warn }, "debug"); + + // When info and debug messages are emitted + logger.info("info"); + logger.debug("debug"); + + // Then both messages are forwarded through the info channel + expect(info.mock.calls).toEqual([["info"], ["debug"]]); + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/test/openclaw-runtime.test.ts b/test/openclaw-runtime.test.ts new file mode 100644 index 0000000..0179d1e --- /dev/null +++ b/test/openclaw-runtime.test.ts @@ -0,0 +1,159 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const originalCwd = process.cwd(); + +async function loadRuntimeModule(options: { packageResolve?: string | Error } = {}) { + vi.resetModules(); + vi.doMock("node:module", () => ({ + createRequire: () => ({ + resolve: () => { + if (options.packageResolve instanceof Error) { + throw options.packageResolve; + } + if (typeof options.packageResolve === "string") { + return options.packageResolve; + } + throw new Error("openclaw package.json was not found"); + }, + }), + })); + return import("../src/openclaw-runtime.ts"); +} + +afterEach(() => { + process.chdir(originalCwd); + vi.doUnmock("node:module"); + vi.doUnmock("../src/openclaw-runtime.ts"); +}); + +describe("openclaw runtime helpers", () => { + it("reads the package name and version from package.json", async () => { + // Given a package.json file with explicit name and version fields + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-runtime-pkg-")); + const packageJsonPath = path.join(tempDir, "package.json"); + fs.writeFileSync( + packageJsonPath, + JSON.stringify({ name: "openclaw", version: "1.2.3" }), + "utf8", + ); + + const runtime = await loadRuntimeModule(); + + // When package metadata is read from that file + const name = runtime.readPackageName(packageJsonPath); + const version = runtime.readPackageVersion(packageJsonPath); + + // Then the declared package name and version are returned + expect(name).toBe("openclaw"); + expect(version).toBe("1.2.3"); + }); + + it("normalizes relative import paths with a leading dot", async () => { + // Given a runtime helper and source and target files in sibling and same directories + const runtime = await loadRuntimeModule(); + + // When relative import paths are normalized + const siblingImport = runtime.normalizeRelativeImportPath("/tmp/a/b", "/tmp/a/c/tool.ts"); + const sameDirImport = runtime.normalizeRelativeImportPath("/tmp/a/b", "/tmp/a/b/tool.ts"); + + // Then each result uses a relative posix path with a leading dot + expect(siblingImport).toBe("../c/tool.ts"); + expect(sameDirImport).toBe("./tool.ts"); + }); + + it("finds the OpenClaw root from cwd when package resolution is unavailable", async () => { + // Given package resolution failure and a cwd nested under an OpenClaw checkout + const openClawRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-root-")); + fs.writeFileSync( + path.join(openClawRoot, "package.json"), + JSON.stringify({ name: "openclaw" }), + "utf8", + ); + const nestedDir = path.join(openClawRoot, "src", "agents"); + fs.mkdirSync(nestedDir, { recursive: true }); + process.chdir(nestedDir); + + const runtime = await loadRuntimeModule({ + packageResolve: new Error("not found"), + }); + + // When the OpenClaw root directory is resolved + const resolvedRoot = runtime.getResolvedOpenClawRootDir(); + + // Then the checkout root is discovered by walking up from cwd + expect(resolvedRoot).toBe(fs.realpathSync(openClawRoot)); + }); + + it("throws a helpful error when no OpenClaw root can be found", async () => { + // Given package resolution failure and a cwd outside any OpenClaw checkout + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-missing-")); + process.chdir(tempDir); + + const runtime = await loadRuntimeModule({ + packageResolve: new Error("not found"), + }); + + // When the OpenClaw root directory is resolved + const resolveRoot = () => runtime.getResolvedOpenClawRootDir(); + + // Then a helpful root-resolution error is thrown + expect(resolveRoot).toThrow( + "agent-control: unable to resolve openclaw package root for internal tool schema access", + ); + }); + + it("returns the first importable JavaScript candidate", async () => { + // Given candidate module paths where only the second JavaScript file exists + const openClawRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-import-js-")); + fs.mkdirSync(path.join(openClawRoot, "dist"), { recursive: true }); + fs.writeFileSync(path.join(openClawRoot, "dist", "candidate.mjs"), "export const value = 123;\n", "utf8"); + + const runtime = await loadRuntimeModule(); + + // When OpenClaw internals are imported through the JavaScript candidate list + const imported = await runtime.tryImportOpenClawInternalModule(openClawRoot, [ + "dist/missing.mjs", + "dist/candidate.mjs", + ]); + + // Then the first importable JavaScript module is returned + expect(imported).toMatchObject({ value: 123 }); + }); + + it("loads a TypeScript candidate through jiti", async () => { + // Given a TypeScript candidate file under the OpenClaw source tree + const openClawRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-import-ts-")); + fs.mkdirSync(path.join(openClawRoot, "src"), { recursive: true }); + fs.writeFileSync(path.join(openClawRoot, "src", "candidate.ts"), "export const value = 123;\n", "utf8"); + + const runtime = await loadRuntimeModule(); + + // When the runtime imports OpenClaw internals from TypeScript candidates + const imported = await runtime.importOpenClawInternalModule(openClawRoot, ["src/candidate.ts"]); + + // Then the TypeScript module is loaded successfully through jiti + expect(imported).toMatchObject({ value: 123 }); + }); + + it("names attempted candidates when no module can be imported", async () => { + // Given an OpenClaw root with no importable internal module candidates + const openClawRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-import-missing-")); + const runtime = await loadRuntimeModule(); + + // When the runtime attempts to import from the candidate list + const importPromise = runtime.importOpenClawInternalModule(openClawRoot, [ + "src/missing.ts", + "dist/missing.js", + ]); + + // Then the thrown error names the attempted candidate paths + await expect( + importPromise, + ).rejects.toThrow( + `agent-control: openclaw internal module not found (src/missing.ts, dist/missing.js) under ${openClawRoot}`, + ); + }); +}); diff --git a/test/session-context.test.ts b/test/session-context.test.ts new file mode 100644 index 0000000..7bf485e --- /dev/null +++ b/test/session-context.test.ts @@ -0,0 +1,189 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { resolveSessionIdentityMock } = vi.hoisted(() => ({ + resolveSessionIdentityMock: vi.fn(), +})); + +vi.mock("../src/session-store.ts", () => ({ + resolveSessionIdentity: resolveSessionIdentityMock, +})); + +import { buildEvaluationContext } from "../src/session-context.ts"; + +function createApi(): OpenClawPluginApi { + return { + id: "agent-control-openclaw-plugin", + version: "test-version", + config: {}, + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + on: vi.fn(), + }; +} + +beforeEach(() => { + resolveSessionIdentityMock.mockReset().mockResolvedValue({ + provider: null, + type: "unknown", + channelName: null, + dmUserName: null, + label: null, + from: null, + to: null, + accountId: null, + source: "unknown", + }); +}); + +describe("buildEvaluationContext", () => { + it("derives channel details from the session key when store metadata is unknown", async () => { + // Given a request with a Discord channel session key and no session-store identity + const request = { + api: createApi(), + sourceAgentId: "worker-1", + state: { + sourceAgentId: "worker-1", + agentName: "base-agent:worker-1", + steps: [], + stepsHash: "hash-1", + lastSyncedStepsHash: null, + syncPromise: null, + }, + event: { + runId: "event-run", + toolCallId: "event-call", + }, + ctx: { + sessionKey: "agent:worker-1:discord:guild-1:channel-2", + }, + failClosed: false, + }; + + // When the evaluation context is built + const context = await buildEvaluationContext(request); + + // Then channel metadata is derived directly from the session key + expect(context).toMatchObject({ + openclawAgentId: "worker-1", + channelType: "channel", + runId: "event-run", + toolCallId: "event-call", + channel: { + provider: "discord", + type: "channel", + scope: "discord:guild-1:channel-2", + source: "sessionKey", + }, + }); + }); + + it("prefers session-store provider and type while retaining the key scope", async () => { + // Given direct-message identity from the session store and a group-scoped session key + resolveSessionIdentityMock.mockResolvedValueOnce({ + provider: "slack", + type: "direct", + channelName: null, + dmUserName: "Alice", + label: "Alice", + from: "alice@example.com", + to: "bot@example.com", + accountId: "acct-1", + source: "sessionStore", + }); + + const request = { + api: createApi(), + sourceAgentId: "worker-1", + state: { + sourceAgentId: "worker-1", + agentName: "base-agent:worker-1", + steps: [{ type: "tool" as const, name: "shell" }], + stepsHash: "hash-1", + lastSyncedStepsHash: "hash-0", + syncPromise: null, + }, + event: {}, + ctx: { + sessionKey: "agent:worker-1:discord:group:team-room", + runId: "ctx-run", + toolCallId: "ctx-call", + }, + failClosed: true, + configuredAgentId: "configured-agent", + configuredAgentVersion: "2026.03.20", + pluginVersion: "test-version", + }; + + // When the evaluation context is built + const context = await buildEvaluationContext(request); + + // Then provider and channel type come from the store while scope stays from the key + expect(context).toMatchObject({ + runId: "ctx-run", + toolCallId: "ctx-call", + channelType: "direct", + dmUserName: "Alice", + senderFrom: "alice@example.com", + policy: { + failClosed: true, + configuredAgentId: "configured-agent", + configuredAgentVersion: "2026.03.20", + }, + sync: { + agentName: "base-agent:worker-1", + stepCount: 1, + stepsHash: "hash-1", + lastSyncedStepsHash: "hash-0", + }, + channel: { + provider: "slack", + type: "direct", + scope: "discord:group:team-room", + source: "sessionStore+sessionKey", + dmUserName: "Alice", + from: "alice@example.com", + }, + }); + }); + + it("falls back to unknown channel information for an unparseable session key", async () => { + // Given a request whose session key does not match the expected agent format + const request = { + api: createApi(), + sourceAgentId: "worker-1", + state: { + sourceAgentId: "worker-1", + agentName: "base-agent:worker-1", + steps: [], + stepsHash: "hash-1", + lastSyncedStepsHash: null, + syncPromise: null, + }, + event: {}, + ctx: { + sessionKey: "not-an-agent-session-key", + }, + failClosed: false, + }; + + // When the evaluation context is built + const context = await buildEvaluationContext(request); + + // Then channel-related fields fall back to unknown values + expect(context).toMatchObject({ + channelType: "unknown", + channelName: null, + dmUserName: null, + senderFrom: null, + channel: { + provider: null, + type: "unknown", + scope: null, + source: "unknown", + }, + }); + }); +}); diff --git a/test/session-store.test.ts b/test/session-store.test.ts new file mode 100644 index 0000000..6814241 --- /dev/null +++ b/test/session-store.test.ts @@ -0,0 +1,230 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +type SessionStoreFixture = { + config?: Record; + initialStore?: Record; + throws?: boolean; +}; + +async function loadSessionStoreModule(fixture: SessionStoreFixture = {}) { + vi.resetModules(); + + let currentStore = fixture.initialStore ?? {}; + const loadConfig = vi.fn(() => fixture.config ?? {}); + const resolveStorePath = vi.fn((storePath?: string) => storePath ?? "/tmp/session-store.json"); + const loadSessionStore = vi.fn(() => currentStore); + const importOpenClawInternalModule = vi.fn(async (_openClawRoot: string, candidates: string[]) => { + if (fixture.throws) { + throw new Error("internal module load failed"); + } + if (candidates.some((candidate) => candidate.includes("sessions"))) { + return { resolveStorePath, loadSessionStore }; + } + return { loadConfig }; + }); + + vi.doMock("../src/openclaw-runtime.ts", () => ({ + getResolvedOpenClawRootDir: () => "/openclaw", + importOpenClawInternalModule, + })); + + const module = await import("../src/session-store.ts"); + return { + resolveSessionIdentity: module.resolveSessionIdentity, + mocks: { + importOpenClawInternalModule, + loadConfig, + resolveStorePath, + loadSessionStore, + setStore(store: Record) { + currentStore = store; + }, + }, + }; +} + +afterEach(() => { + vi.useRealTimers(); + vi.doUnmock("../src/openclaw-runtime.ts"); +}); + +describe("resolveSessionIdentity", () => { + it("returns an unknown identity when no session key is provided", async () => { + // Given the session-store resolver with no session key input + const { resolveSessionIdentity } = await loadSessionStoreModule(); + + // When session identity is resolved + const identityPromise = resolveSessionIdentity(undefined); + + // Then an unknown identity object is returned + await expect(identityPromise).resolves.toEqual({ + provider: null, + type: "unknown", + channelName: null, + dmUserName: null, + label: null, + from: null, + to: null, + accountId: null, + source: "unknown", + }); + }); + + it("maps direct-message metadata from the session store", async () => { + // Given a session-store entry for a direct-message conversation + const { resolveSessionIdentity } = await loadSessionStoreModule({ + initialStore: { + "agent:worker-1:slack:direct:alice": { + origin: { + provider: "slack", + chatType: "direct", + label: "Alice", + from: "alice@example.com", + to: "bot@example.com", + accountId: "acct-1", + }, + displayName: "Alice Display", + }, + }, + }); + + // When identity is resolved for that direct-message session key + const identityPromise = resolveSessionIdentity("agent:worker-1:slack:direct:alice"); + + // Then the direct-message metadata is mapped into the returned identity + await expect(identityPromise).resolves.toEqual({ + provider: "slack", + type: "direct", + channelName: null, + dmUserName: "Alice", + label: "Alice", + from: "alice@example.com", + to: "bot@example.com", + accountId: "acct-1", + source: "sessionStore", + }); + }); + + it("reuses base session metadata for thread-specific keys", async () => { + // Given only a base channel session entry and a thread-specific lookup key + const { resolveSessionIdentity } = await loadSessionStoreModule({ + initialStore: { + "agent:worker-1:slack:channel:eng": { + origin: { + provider: "slack", + chatType: "channel", + label: "Engineering", + }, + groupChannel: "eng", + }, + }, + }); + + // When identity is resolved for the thread-specific key + const identityPromise = resolveSessionIdentity("agent:worker-1:slack:channel:eng:thread:123"); + + // Then the base session metadata is reused for the thread + await expect(identityPromise).resolves.toMatchObject({ + provider: "slack", + type: "channel", + channelName: "eng", + label: "Engineering", + source: "sessionStore", + }); + }); + + it("reuses the cached identity before the TTL expires", async () => { + // Given cached session metadata and a TTL window that has not expired + vi.useFakeTimers(); + const { resolveSessionIdentity, mocks } = await loadSessionStoreModule({ + initialStore: { + "agent:worker-1:slack:direct:alice": { + origin: { + provider: "slack", + chatType: "direct", + label: "Alice", + }, + }, + }, + }); + + // When the same session is resolved twice after the backing store changes + const first = await resolveSessionIdentity("agent:worker-1:slack:direct:alice"); + mocks.setStore({ + "agent:worker-1:slack:direct:alice": { + origin: { + provider: "slack", + chatType: "direct", + label: "Bob", + }, + }, + }); + const second = await resolveSessionIdentity("agent:worker-1:slack:direct:alice"); + + // Then the cached identity is reused and the store is only loaded once + expect(first.label).toBe("Alice"); + expect(second.label).toBe("Alice"); + expect(mocks.loadSessionStore).toHaveBeenCalledTimes(1); + }); + + it("refreshes the identity after the TTL expires", async () => { + // Given cached session metadata and a store update after the TTL window + vi.useFakeTimers(); + const { resolveSessionIdentity, mocks } = await loadSessionStoreModule({ + initialStore: { + "agent:worker-1:slack:direct:alice": { + origin: { + provider: "slack", + chatType: "direct", + label: "Alice", + }, + }, + }, + }); + + // When the session is resolved again after advancing past the TTL + await expect(resolveSessionIdentity("agent:worker-1:slack:direct:alice")).resolves.toMatchObject({ + label: "Alice", + }); + + mocks.setStore({ + "agent:worker-1:slack:direct:alice": { + origin: { + provider: "slack", + chatType: "direct", + label: "Bob", + }, + }, + }); + vi.advanceTimersByTime(2_001); + + // Then the refreshed identity is returned and the store is reloaded + await expect(resolveSessionIdentity("agent:worker-1:slack:direct:alice")).resolves.toMatchObject({ + label: "Bob", + }); + expect(mocks.loadSessionStore).toHaveBeenCalledTimes(2); + }); + + it("returns an unknown identity when session-store internals cannot be loaded", async () => { + // Given a runtime fixture where OpenClaw session-store internals fail to load + const { resolveSessionIdentity } = await loadSessionStoreModule({ + throws: true, + }); + + // When identity is resolved for any session key + const identityPromise = resolveSessionIdentity("agent:worker-1:slack:direct:alice"); + + // Then the resolver falls back to an unknown identity + await expect(identityPromise).resolves.toEqual({ + provider: null, + type: "unknown", + channelName: null, + dmUserName: null, + label: null, + from: null, + to: null, + accountId: null, + source: "unknown", + }); + }); +}); diff --git a/test/shared.test.ts b/test/shared.test.ts new file mode 100644 index 0000000..a4023b4 --- /dev/null +++ b/test/shared.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { + asPositiveInt, + asString, + formatToolArgsForLog, + hashSteps, + sanitizeToolCatalogConfig, + toJsonRecord, +} from "../src/shared.ts"; + +describe("shared utilities", () => { + it("returns undefined for a blank string", () => { + // Given a string that only contains whitespace + const value = " "; + + // When the string is normalized + const normalized = asString(value); + + // Then the helper returns undefined + expect(normalized).toBeUndefined(); + }); + + it("floors a positive floating-point number", () => { + // Given a positive floating-point number + const value = 42.9; + + // When the number is normalized as a positive integer + const normalized = asPositiveInt(value); + + // Then the fractional portion is discarded + expect(normalized).toBe(42); + }); + + it("returns undefined for a non-record JSON value", () => { + // Given a JSON value that is an array instead of an object record + const value = ["not", "a", "record"]; + + // When the value is coerced to a JSON record + const record = toJsonRecord(value); + + // Then no record is returned + expect(record).toBeUndefined(); + }); + + it("forces plugins off while preserving sibling config", () => { + // Given plugin config with plugins enabled and sibling settings present + const config = { + mode: "test", + plugins: { + enabled: true, + keepMe: "yes", + }, + }; + + // When the tool catalog config is sanitized + const sanitized = sanitizeToolCatalogConfig(config); + + // Then plugins are forced off and unrelated settings are preserved + expect(sanitized).toEqual({ + mode: "test", + plugins: { + enabled: false, + keepMe: "yes", + }, + }); + }); + + it("returns a stable placeholder for unserializable arguments", () => { + // Given a circular argument payload that cannot be JSON serialized + const circular: { self?: unknown } = {}; + circular.self = circular; + + // When the payload is formatted for logging + const formatted = formatToolArgsForLog(circular); + + // Then a stable placeholder string is returned + expect(formatted).toBe("[unserializable]"); + }); + + it("produces the same digest for identical steps", () => { + // Given the same step list hashed twice + const steps = [{ type: "tool" as const, name: "shell" }]; + + // When digests are computed for both hashes + const first = hashSteps(steps); + const second = hashSteps(steps); + + // Then both digests are identical + expect(first).toBe(second); + }); +}); diff --git a/test/tool-catalog.test.ts b/test/tool-catalog.test.ts new file mode 100644 index 0000000..791aa6b --- /dev/null +++ b/test/tool-catalog.test.ts @@ -0,0 +1,223 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +type ToolCatalogFixture = { + openClawRoot: string; + distPiToolsModule?: Record | null; + distAdapterModule?: Record | null; + sourcePiToolsModule?: Record; + sourceAdapterModule?: Record; +}; + +async function loadToolCatalogModule(fixture: ToolCatalogFixture) { + vi.resetModules(); + + const tryImportOpenClawInternalModule = vi.fn( + async (_openClawRoot: string, candidates: string[]) => { + if (candidates.some((candidate) => candidate.includes("pi-tools"))) { + return fixture.distPiToolsModule ?? null; + } + return fixture.distAdapterModule ?? null; + }, + ); + + const importOpenClawInternalModule = vi.fn( + async (_openClawRoot: string, candidates: string[]) => { + if (candidates.some((candidate) => candidate.includes("pi-tools"))) { + return fixture.sourcePiToolsModule ?? {}; + } + return fixture.sourceAdapterModule ?? {}; + }, + ); + + vi.doMock("../src/openclaw-runtime.ts", () => ({ + getResolvedOpenClawRootDir: () => fixture.openClawRoot, + tryImportOpenClawInternalModule, + importOpenClawInternalModule, + normalizeRelativeImportPath: vi.fn((fromDir: string, toFile: string) => + path.relative(fromDir, toFile), + ), + PLUGIN_ROOT_DIR: path.join(fixture.openClawRoot, "plugin"), + readPackageVersion: vi.fn(() => "1.0.0"), + safeStatMtimeMs: vi.fn(() => null), + })); + + const module = await import("../src/tool-catalog.ts"); + return { + resolveStepsForContext: module.resolveStepsForContext, + mocks: { + tryImportOpenClawInternalModule, + importOpenClawInternalModule, + }, + }; +} + +function createApi(config: Record): OpenClawPluginApi { + return { + id: "agent-control-openclaw-plugin", + version: "test-version", + config, + logger: { + info: vi.fn(), + warn: vi.fn(), + }, + on: vi.fn(), + }; +} + +function createLogger() { + return { + info: vi.fn(), + debug: vi.fn(), + warn: vi.fn(), + block: vi.fn(), + }; +} + +afterEach(() => { + vi.doUnmock("../src/openclaw-runtime.ts"); +}); + +describe("resolveStepsForContext", () => { + it("deduplicates definitions and disables plugins in synced config", async () => { + // Given duplicate, invalid, and blank tool definitions from OpenClaw internals + const createOpenClawCodingTools = vi.fn(() => ["tool-marker"]); + const toToolDefinitions = vi.fn(() => [ + { + name: "shell", + label: "Shell v1", + description: "Run a shell command", + parameters: { type: "object", title: "v1" }, + }, + { + name: "shell", + label: "Shell v2", + description: "Run a newer shell command", + parameters: { type: "object", title: "v2" }, + }, + { + name: "browser", + label: "Browser", + parameters: ["not-a-record"], + }, + { + name: " ", + label: "Ignored", + }, + ]); + + const { resolveStepsForContext, mocks } = await loadToolCatalogModule({ + openClawRoot: fs.mkdtempSync(path.join(os.tmpdir(), "tool-catalog-dist-")), + distPiToolsModule: { createOpenClawCodingTools }, + distAdapterModule: { toToolDefinitions }, + }); + const logger = createLogger(); + + // When steps are resolved for the source agent and session context + const steps = await resolveStepsForContext({ + api: createApi({ + plugins: { + enabled: true, + keepMe: "yes", + }, + mode: "test", + }), + logger, + sourceAgentId: "worker-1", + sessionKey: "agent:worker-1:slack:direct:alice", + sessionId: "session-1", + runId: "run-1", + }); + + // Then the last valid definition wins and synced config disables plugins + expect(steps).toEqual([ + { + type: "tool", + name: "shell", + description: "Run a newer shell command", + inputSchema: { type: "object", title: "v2" }, + metadata: { label: "Shell v2" }, + }, + { + type: "tool", + name: "browser", + description: "Browser", + metadata: { label: "Browser" }, + }, + ]); + expect(createOpenClawCodingTools).toHaveBeenCalledWith( + expect.objectContaining({ + agentId: "worker-1", + sessionKey: "agent:worker-1:slack:direct:alice", + sessionId: "session-1", + runId: "run-1", + senderIsOwner: true, + config: { + plugins: { + enabled: false, + keepMe: "yes", + }, + mode: "test", + }, + }), + ); + expect(mocks.importOpenClawInternalModule).not.toHaveBeenCalled(); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining("resolve_steps duration_sec="), + ); + }); + + it("falls back to source modules when dist internals are unavailable", async () => { + // Given a fixture where dist internals are missing but source modules are available + const openClawRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tool-catalog-source-")); + const createOpenClawCodingTools = vi.fn(() => ["tool-marker"]); + const toToolDefinitions = vi.fn(() => [ + { + name: "shell", + label: "Shell", + description: "Run a shell command", + parameters: { type: "object" }, + }, + ]); + + const { resolveStepsForContext, mocks } = await loadToolCatalogModule({ + openClawRoot, + distPiToolsModule: null, + distAdapterModule: null, + sourcePiToolsModule: { createOpenClawCodingTools }, + sourceAdapterModule: { toToolDefinitions }, + }); + + // When steps are resolved for the source agent + const steps = await resolveStepsForContext({ + api: createApi({}), + logger: createLogger(), + sourceAgentId: "worker-1", + }); + + // Then the source-module fallback is used to build tool steps + expect(steps).toEqual([ + { + type: "tool", + name: "shell", + description: "Run a shell command", + inputSchema: { type: "object" }, + metadata: { label: "Shell" }, + }, + ]); + expect(mocks.importOpenClawInternalModule).toHaveBeenCalledTimes(2); + expect(mocks.importOpenClawInternalModule).toHaveBeenNthCalledWith( + 1, + openClawRoot, + ["src/agents/pi-tools.ts"], + ); + expect(mocks.importOpenClawInternalModule).toHaveBeenNthCalledWith( + 2, + openClawRoot, + ["src/agents/pi-tool-definition-adapter.ts"], + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..491049d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowImportingTsExtensions": true, + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "types": ["node", "vitest/globals"] + }, + "include": ["index.ts", "src/**/*.ts", "test/**/*.ts", "types/**/*.d.ts"] +} diff --git a/types/openclaw-plugin-sdk-core.d.ts b/types/openclaw-plugin-sdk-core.d.ts new file mode 100644 index 0000000..7fd01fc --- /dev/null +++ b/types/openclaw-plugin-sdk-core.d.ts @@ -0,0 +1,36 @@ +declare module "openclaw/plugin-sdk/core" { + export type OpenClawBeforeToolCallEvent = { + toolName: string; + params?: unknown; + runId?: string; + toolCallId?: string; + }; + + export type OpenClawBeforeToolCallContext = { + agentId?: string; + sessionKey?: string; + sessionId?: string; + runId?: string; + toolCallId?: string; + }; + + export interface OpenClawPluginApi { + id: string; + version?: string; + config: Record; + pluginConfig?: unknown; + logger: { + info(message: string): void; + warn(message: string): void; + }; + on(event: "gateway_start", handler: () => void | Promise): void; + on( + event: "before_tool_call", + handler: ( + event: OpenClawBeforeToolCallEvent, + ctx: OpenClawBeforeToolCallContext, + ) => unknown, + ): void; + on(event: string, handler: (...args: any[]) => unknown): void; + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7e50ed5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + fileParallelism: false, + coverage: { + provider: "v8", + all: true, + reporter: ["text", "html", "json-summary", "lcov"], + reportsDirectory: "./coverage", + include: ["index.ts", "src/**/*.ts"], + exclude: ["index.ts", "src/types.ts", "test/**/*.ts", "types/**/*.d.ts"], + }, + }, +});