diff --git a/.github/actions/e2e/action.yml b/.github/actions/e2e/action.yml new file mode 100644 index 00000000..f4834692 --- /dev/null +++ b/.github/actions/e2e/action.yml @@ -0,0 +1,84 @@ +name: Extension end-to-end tests +description: Test an existing VSIX in isolated VS Code and optionally run the browser suite. + +inputs: + vsix: + description: Exact VSIX produced by the caller's packaging step. + required: true + vscode-version: + description: Exact VS Code version or latest; empty uses engines.vscode's minimum. + default: '' + browser-tests: + description: Also run the browser-only Playwright suite. + default: 'true' + +runs: + using: composite + steps: + - name: Cache desktop downloads + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 + with: + path: .vscode-test/cache + key: vscode-${{ runner.os }}-${{ runner.arch }}-${{ inputs.vscode-version || 'minimum' }}-${{ hashFiles('package.json', 'package-lock.json') }}-${{ inputs.vscode-version == 'latest' && github.run_id || 'pinned' }} + restore-keys: | + vscode-${{ runner.os }}-${{ runner.arch }}-${{ inputs.vscode-version || 'minimum' }}-${{ hashFiles('package.json', 'package-lock.json') }}- + + - name: Install Linux display and browser libraries + if: runner.os == 'Linux' + shell: bash + run: npx playwright install-deps chromium + + - name: Install browser test Chromium + if: inputs.browser-tests == 'true' + shell: bash + run: npx playwright install chromium + + - name: Browser webview tests + if: inputs.browser-tests == 'true' + shell: bash + run: npm run test:e2e + + - name: Packaged VSIX smoke tests + shell: bash + env: + TEST_VSIX: ${{ inputs.vsix }} + TEST_VSCODE_VERSION: ${{ inputs.vscode-version }} + run: | + args=(--vsix "$TEST_VSIX") + if [ -n "$TEST_VSCODE_VERSION" ]; then + args+=(--vscode-version "$TEST_VSCODE_VERSION") + fi + if [ "$RUNNER_OS" = Linux ]; then + xvfb-run -a --server-args="-screen 0 1280x900x24" npm run test:vscode -- "${args[@]}" + else + npm run test:vscode -- "${args[@]}" + fi + + - name: Upload test diagnostics + id: diagnostics + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: e2e-${{ runner.os }}-${{ inputs.vscode-version || 'minimum' }} + path: test-results/ + if-no-files-found: warn + retention-days: 7 + + - name: Summarize desktop snapshots + if: always() + shell: bash + env: + E2E_ARTIFACT_URL: ${{ steps.diagnostics.outputs.artifact-url }} + run: | + shopt -s nullglob + reports=(test-results/vscode/*/snapshots.md) + if [ "${#reports[@]}" -eq 0 ]; then + printf '## VS Code smoke snapshots\n\nNo desktop snapshot report was produced. Check the test-step logs.\n' >> "$GITHUB_STEP_SUMMARY" + else + for report in "${reports[@]}"; do + cat "$report" >> "$GITHUB_STEP_SUMMARY" + done + fi + if [ -n "$E2E_ARTIFACT_URL" ]; then + printf '\n[Download screenshots and test diagnostics](%s)\n\nExtract the artifact and open `vscode//gallery.html` to browse the milestone snapshots. Individual PNGs are under each suite'\''s `snapshots` directory; failure screenshots remain under `screenshots`.\n' "$E2E_ARTIFACT_URL" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9d8dd7c..0e67b2b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ permissions: jobs: verify: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout @@ -32,14 +33,23 @@ jobs: - name: Build run: npm run build - - name: Check bundle size - run: npm run check-size - - name: Package VSIX run: npm run package + - name: Check bundle and VSIX size + run: npm run check-size + + - name: Capture VSIX path + id: vsix + run: echo "path=$(node -p 'const p = require("./package.json"); `${p.name}-${p.version}.vsix`')" >> "$GITHUB_OUTPUT" + + - name: End-to-end tests + uses: ./.github/actions/e2e + with: + vsix: ${{ steps.vsix.outputs.path }} + - name: Upload VSIX artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: vsix - path: '*.vsix' + path: ${{ steps.vsix.outputs.path }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e630e138..309effb6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ permissions: jobs: release: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout @@ -39,8 +40,8 @@ jobs: - name: Run build run: npm run build - - name: Run tests - run: npm test + - name: Run checks + run: npm run check - name: Package VSIX run: npm run package @@ -48,13 +49,18 @@ jobs: - name: Capture VSIX path id: vsix run: | - VSIX_PATH="$(find . -maxdepth 1 -name '*.vsix' -print -quit)" - if [ -z "$VSIX_PATH" ]; then - echo "No VSIX package was produced" - exit 1 - fi + VSIX_PATH="$(node -p 'const p = require("./package.json"); `${p.name}-${p.version}.vsix`')" + test -f "$VSIX_PATH" echo "path=$VSIX_PATH" >> "$GITHUB_OUTPUT" + - name: Check bundle and VSIX size + run: npm run check-size + + - name: End-to-end release gate + uses: ./.github/actions/e2e + with: + vsix: ${{ steps.vsix.outputs.path }} + - name: Create GitHub release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 @@ -68,4 +74,3 @@ jobs: with: name: vsix path: ${{ steps.vsix.outputs.path }} - diff --git a/.github/workflows/vscode-compatibility.yml b/.github/workflows/vscode-compatibility.yml new file mode 100644 index 00000000..c3dd228b --- /dev/null +++ b/.github/workflows/vscode-compatibility.yml @@ -0,0 +1,52 @@ +name: VS Code compatibility + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/vscode-compatibility.yml' + - '.github/actions/e2e/**' + - 'tests/vscode/**' + schedule: + - cron: '0 7 * * 1' + +permissions: + contents: read + +jobs: + smoke: + name: ${{ matrix.os }} / VS Code ${{ matrix.version || 'minimum' }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + version: ['', latest] + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Package VSIX + run: npm run package + + - name: Capture VSIX path + id: vsix + shell: bash + run: echo "path=$(node -p 'const p = require("./package.json"); `${p.name}-${p.version}.vsix`')" >> "$GITHUB_OUTPUT" + + - name: Desktop compatibility tests + uses: ./.github/actions/e2e + with: + vsix: ${{ steps.vsix.outputs.path }} + vscode-version: ${{ matrix.version }} + browser-tests: 'false' diff --git a/.vscodeignore b/.vscodeignore index 2643c1ec..010937ec 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -10,6 +10,7 @@ eslint.config.mjs # Dev state .vscode/** +.vscode-test/** .github/** .git/** .gitignore @@ -21,6 +22,7 @@ eslint.config.mjs **/*.map *.vsix dist/**/*.map +test-results/** # Docs scaffolding (only publish README + LICENSE + dist) README.github.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca6288e4..7a7864b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,130 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio Please use [GitHub Issues](https://github.com/microsoft/ai-engineer-coach/issues) to report bugs or request features. Before filing a new issue, please check if one already exists. +## End-to-End Testing + +Use Node 22 (the CI version) or a newer supported Node release. Install the locked dependencies +with `npm ci`. The existing database integration tests also require the `sqlite3` command on +your `PATH`. There are three complementary test layers: + +| Layer | Command | What it verifies | +| --- | --- | --- | +| Unit and fixture tests | `npm test` | Parsers, rules, calculations, and synthetic-data invariants | +| Browser webview tests | `npm run build` then `npm run test:e2e` | Dashboard interactions with mocked host responses | +| Packaged VS Code tests | `npm run test:vscode` | Installation, activation, workers, real webview messaging, navigation, reload, and empty data | + +Before the first browser run, install its browser with `npx playwright install chromium`. +On a Linux machine without a display, install the desktop dependencies with +`npx playwright install-deps chromium`, then run `xvfb-run -a npm run test:vscode`. +Windows and macOS run the same npm command without the virtual display. + +### Testing the shipped extension + +`npm run test:vscode` compiles the test code, packages the extension through the normal packaging +script, and installs that VSIX into a disposable VS Code installation. It never installs into +your normal editor. To test an already-built artifact without rebuilding it, use: + +```sh +npm run test:vscode -- --vsix ai-engineer-coach-0.1.0.vsix +``` + +Substitute the filename produced by `npm run package` when the package version changes. +The runner records the package's SHA-256 hash and checks that the artifact did not change. +CI and releases pass the exact package path this way; publishing happens only after the +browser and desktop suites pass, and the release uses that same VSIX. + +By default, desktop tests use the minimum version in `package.json`'s `engines.vscode`. +Use `--vscode-version 1.125.0` to select an explicit compatible version, or +`--vscode-version latest` to check the current stable release. An incompatible pinned version +fails before launching. The [compatibility workflow](.github/workflows/vscode-compatibility.yml) +runs the minimum and latest versions on Linux, Windows, and macOS weekly, on manual dispatch, +and on PRs that change the desktop harness, its fixtures, or the shared E2E action. + +The desktop runner uses [ExTester](https://github.com/redhat-developer/vscode-extension-tester). +Its actively tested editor-version window moves with new releases; older versions are +best-effort. Keep the minimum-version tests rather than silently increasing our public engine +requirement to accommodate the runner. Dependency updates use the existing Dependabot workflow. + +### Synthetic data and isolation + +The checked-in data is [`tests/vscode/data/projects.json`](tests/vscode/data/projects.json): +fictional project names, prompts, source code, tests, model IDs, and a seven-day activity schedule. +It is entirely synthetic, not sampled or anonymized from anyone's personal logs. Three projects +cover TypeScript (`inventory-api`), Python (`sensor-pipeline`), and Go (`release-worker`). + +[`tests/vscode/fixtures.ts`](tests/vscode/fixtures.ts) materializes those definitions into raw +`chatSessions` and `chatEditingSessions` JSON in the platform's normal discovery layout, +entirely inside a fresh temporary home. Initially there are **21 sessions, 63 requests, 315 added +lines and 294 net lines** across seven active days, including weekday, weekend and evening activity. +Each session records file creation, a one-line correction, and test creation; the same code also +appears in responses to exercise edit/code-block deduplication. The reload test adds one session, +then verifies **22 sessions, 66 requests and 330 added lines**, including a second reload without +duplicates. The empty-data scenario starts a separate process with no session sources. + +The data is varied but deterministic rather than randomly changing on every build. The most +recent activity day is the day before the run, with earlier days and local hours taken from the +schedule, so charts remain populated. The unit suite checks exact code totals, all seven heatmap +days, overlapping Timeline sessions, and the three project summaries through the real parser +and analyzers. Desktop tests do not replace the parser, workers, VS Code API, or message bridge. +They also check that input log files remain byte-for-byte unchanged except for the test's +explicit addition. Personal home, AppData, Copilot data, temporary directories, extension +directories, and VS Code profiles are isolated; no sign-in or live model call is required. + +Only editor/driver download archives are cached under `.vscode-test/cache`. Each invocation +creates fresh profiles, fixtures, and application caches and removes them after the run. +Diagnostics remain under `test-results/vscode`, including runtime and driver logs, failure +screenshots, and a package-hash summary. Browser traces, screenshots, and the HTML report +are stored separately under `test-results/browser` and `test-results/browser-report`. +GitHub Actions uploads these diagnostics even when a test fails. + +The pipeline logs the generated counts and includes a **Synthetic test data** section in its +summary and gallery. Downloaded artifacts include each suite's `dataset.json`, the checked-in +definitions under `fixtures/projects.json`, the actual initial input copies under +`fixtures/initial`, and the planned additional files under `fixtures/reload`. These are generated +test inputs, not user logs; the reload files are kept separate from the initial dataset. + +Successful milestones also capture real VS Code screenshots: dashboard totals and activity, Timeline +session details, nonzero Output by project, Patterns Work Hours and Projects, reload, reopening, +and the expected empty-data state. In a GitHub Actions +run, open the **Summary** and follow **Download screenshots and test diagnostics**. Extract the +artifact, then open `vscode//gallery.html`; its images are embedded, so it works offline. +The gallery includes captions, suite outcomes, dataset counts, the editor target, and the tested VSIX hash. +Individual PNGs are in each suite's `snapshots` directory. Locally, the same gallery is under +`test-results/vscode//gallery.html`. + +These screenshots document the synthetic-data run, not pixel-comparison baselines. The capture +helper waits for chart pixels to settle so it does not show an in-progress animation. Captures +are preserved even if a later assertion fails; the report still marks that suite as failed. +Existing automatic failure screenshots remain separate. + +### Maintaining regression coverage + +Add the regression test in the same PR as the feature or fix. Prefer the smallest layer that +can reproduce the behavior: parser/calculation changes belong in Vitest, dashboard interaction +changes in Playwright, and packaging or host/webview boundary changes in the desktop suite. +Do not duplicate every browser scenario in VS Code. + +Add lowercase, hyphenated `*.spec.ts` files directly under +[`tests/vscode/suite`](tests/vscode/suite). The runner discovers new source suites automatically; +each file gets a separate process, profile, and baseline synthetic data. `empty.spec.ts` is the +no-data exception. Reuse the [`Dashboard`](tests/vscode/dashboard.ts) helper and small synthetic +fixtures. Keep selectors in the helper, wait for observable readiness instead of fixed sleeps, +and assert exact results. +After a meaningful assertion, use `await dashboard.snapshot('01-feature', 'What the screenshot demonstrates')` +to add a named milestone to the gallery. Use a unique lowercase, hyphenated name within each suite. +The desktop suite is sequential, has no automatic test retries, and rejects focused, skipped, or +empty test runs. Inherited `MOCHA_GREP` and `MOCHA_INVERT` filters are removed so a nonempty subset +cannot pass as the full suite. Reload replaces the webview's frames, so the dashboard helper waits +for a different document and reacquires detached frames within a single deadline, including +ExTester's wrapped stale-outer-frame error. It does not repeat the reload command or suppress +unexpected driver failures. +Its TypeScript and fixture tests are included in `npm run check`. + +Use the shared [E2E action](.github/actions/e2e/action.yml) for pipeline changes so PR, release, +and compatibility runs do not acquire different setup procedures. Investigate failures using +the saved screenshots and logs; do not bypass the release gate or disable an operating-system +security feature globally to get a green run. + ## Security If you discover a security vulnerability, please follow the instructions in [SECURITY.md](SECURITY.md). diff --git a/eslint.config.mjs b/eslint.config.mjs index e50c942b..1a7bc465 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,7 +13,7 @@ export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, { - files: ["src/**/*.ts"], + files: ["src/**/*.ts", "tests/vscode/**/*.ts"], languageOptions: { parserOptions: { projectService: true, @@ -83,6 +83,6 @@ export default tseslint.config( }, }, { - ignores: ["dist/", "node_modules/", "**/*.mjs", "tests/"], + ignores: ["dist/", "node_modules/", ".vscode-test/", "**/*.mjs", "tests/e2e/"], } ); diff --git a/knip.json b/knip.json index 1cd345b2..0ab28cd9 100644 --- a/knip.json +++ b/knip.json @@ -7,9 +7,11 @@ "src/core/warm-up-worker.ts", "src/core/parse-worker.ts", "src/core/cache-write-worker.ts", - "src/core/parser.bench.ts" + "src/core/parser.bench.ts", + "tests/vscode/run.ts", + "tests/vscode/suite/**/*.spec.ts" ], - "project": ["src/**/*.ts"], + "project": ["src/**/*.ts", "tests/vscode/**/*.ts"], "ignore": [ "src/webview/page-insights.ts", "src/webview/page-rule-editor.ts", diff --git a/package-lock.json b/package-lock.json index 38d56ab2..f9ac7bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "devDependencies": { "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", + "@types/mocha": "10.0.10", "@types/node": "26.4.0", "@types/vscode": "1.125.0", "@vitest/coverage-v8": "4.1.11", @@ -35,11 +36,13 @@ "knip": "6.33.0", "lint-staged": "17.4.1", "lockfile-lint": "5.0.1", + "mocha": "12.0.3", "preact-render-to-string": "6.7.0", "serve": "14.2.6", "typescript": "6.0.3", "typescript-eslint": "8.68.0", - "vitest": "4.1.11" + "vitest": "4.1.11", + "vscode-extension-tester": "8.28.1" }, "engines": { "vscode": "^1.125.0" @@ -160,6 +163,16 @@ "node": ">=20.0.0" } }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@azure/core-rest-pipeline": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", @@ -208,26 +221,27 @@ } }, "node_modules/@azure/identity": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", - "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz", + "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==", "dev": true, "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", - "@azure/msal-node": "^5.1.0", - "open": "^10.1.0", + "@azure/msal-node": "^6.0.0", + "open": "^11.0.0", "tslib": "^2.2.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@azure/logger": { @@ -268,13 +282,13 @@ } }, "node_modules/@azure/msal-node": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", - "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.6.2", + "@azure/msal-common": "16.14.1", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -282,9 +296,9 @@ } }, "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", - "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", "dev": true, "license": "MIT", "engines": { @@ -356,6 +370,13 @@ "node": ">=6.9.0" } }, + "node_modules/@bazel/runfiles": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz", + "integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1896,6 +1917,29 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1924,12 +1968,266 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@napi-rs/keyring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" + } + }, + "node_modules/@napi-rs/keyring-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-freebsd-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-arm64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-gnu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-linux-x64-musl": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-arm64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-ia32-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/keyring-win32-x64-msvc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -2654,46 +2952,224 @@ "node": ">=20" } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", - "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", - "cpu": [ - "arm" - ], + "node_modules/@redhat-developer/locators": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/locators/-/locators-1.25.1.tgz", + "integrity": "sha512-jm/Qf9Ycukskadee7MfBDDGVEKbCZaHDiQPnHjy019uAhP9M7E0bBaTcc3GVTEuLP25LIVHyxRr+DQc/Roxsug==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=22" + }, + "peerDependencies": { + "@redhat-developer/page-objects": ">=1.0.0", + "selenium-webdriver": "^4.49.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", - "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", - "cpu": [ - "arm64" - ], + "node_modules/@redhat-developer/page-objects": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@redhat-developer/page-objects/-/page-objects-1.25.1.tgz", + "integrity": "sha512-gz8fwQZPlmNIZHXXucB+YjH5Wv62Eq77d9ExDrQGine0SlaLYv/nlpsJI6insunluVz3zk5m+eTpt+bb9RhA6g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "clipboardy": "^5.3.2", + "clone-deep": "^4.0.1", + "compare-versions": "^6.1.1", + "fs-extra": "^11.4.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=22" + }, + "peerDependencies": { + "selenium-webdriver": "^4.49.0", + "typescript": ">=4.6.2" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", - "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", - "cpu": [ - "arm64" + "node_modules/@redhat-developer/page-objects/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/clipboardy": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-5.3.2.tgz", + "integrity": "sha512-R35PENCHFCw6lsd5SjYPuAVV3Zawr74mKc7ogFNzoDPoQsmWDoJgUNNnWCk/czeqdZGZs8Y0M8zkOlVoySfHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "clipboard-image": "^0.1.0", + "execa": "^9.6.1", + "is-wayland": "^0.1.0", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0", + "powershell-utils": "^0.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@redhat-developer/page-objects/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" ], "dev": true, "license": "MIT", @@ -2934,6 +3410,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -3108,6 +3591,19 @@ "node": ">=20.0.0" } }, + "node_modules/@sindresorhus/is": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-8.1.0.tgz", + "integrity": "sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -3255,6 +3751,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3262,6 +3772,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.4.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", @@ -3286,6 +3803,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/selenium-webdriver": { + "version": "4.35.7", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.35.7.tgz", + "integrity": "sha512-TVduvRhQrIdjPWEC6YJLxwZWegWLD3uDOsqMuWk2U9+TiXw6wAH9RlKEtuHO8kioZkJ5MWGC9pQDY0nXvgi0nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ws": "*" + } + }, "node_modules/@types/vscode": { "version": "1.125.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", @@ -3293,6 +3821,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.68.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", @@ -4051,9 +4589,9 @@ } }, "node_modules/@vscode/vsce-sign": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", - "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", "dev": true, "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", @@ -4540,6 +5078,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -4683,6 +5228,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/browserslist": { "version": "4.28.8", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", @@ -4774,9 +5326,9 @@ } }, "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", "dev": true, "license": "MIT", "dependencies": { @@ -4789,6 +5341,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4799,68 +5364,259 @@ "node": ">= 0.8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/c8": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^18.0.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.12.0 || >=23" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/c8/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=20" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/c8/node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "node_modules/c8/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/c8/node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, "engines": { - "node": ">=14.16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "node_modules/c8/node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, - "funding": [ - { - "type": "opencollective", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/c8/node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/cacheable-request/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { @@ -5002,6 +5758,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -5010,6 +5782,19 @@ "license": "ISC", "optional": true }, + "node_modules/chunk-data": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chunk-data/-/chunk-data-0.1.0.tgz", + "integrity": "sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -5039,6 +5824,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clipboard-image": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clipboard-image/-/clipboard-image-0.1.0.tgz", + "integrity": "sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-jxa": "^3.0.0" + }, + "bin": { + "clipboard-image": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clipboardy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", @@ -5142,6 +5946,21 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -5219,6 +6038,13 @@ "node": ">= 12.0.0" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5336,6 +6162,13 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", @@ -5434,6 +6267,35 @@ "node": ">= 8" } }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cspell": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/cspell/-/cspell-10.1.1.tgz", @@ -5782,9 +6644,9 @@ "license": "MIT" }, "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", "dev": true, "license": "MIT", "dependencies": { @@ -5857,6 +6719,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -5931,6 +6803,49 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6756,6 +7671,35 @@ "walk-up-path": "^4.0.0" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6833,6 +7777,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -6883,9 +7844,9 @@ "optional": true }, "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", "dev": true, "license": "MIT", "dependencies": { @@ -6955,9 +7916,22 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", @@ -7181,6 +8155,86 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-16.1.0.tgz", + "integrity": "sha512-LOvohAoc/3O8+Gi3E5RRvamkyXmJq9ewj/cimmqQYn+Me5OKCVHKQI9ygF0O882l573lrpLbKZpsydYRttpDEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^8.1.0", + "byte-counter": "^0.1.0", + "cacheable-request": "^13.0.19", + "chunk-data": "^0.1.0", + "decompress-response": "^10.0.0", + "keyv": "^5.6.0", + "lowercase-keys": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^5.10.0", + "uint8array-extras": "^1.6.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/got/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got/node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -7262,6 +8316,16 @@ "node": ">=10" } }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/htm": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", @@ -7321,6 +8385,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -7436,6 +8507,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-4.0.0.tgz", @@ -7501,8 +8579,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -7600,6 +8677,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -7629,6 +8719,45 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-port-reachable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", @@ -7675,6 +8804,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wayland": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-wayland/-/is-wayland-0.1.0.tgz", + "integrity": "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -7691,6 +8846,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7698,6 +8876,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -7979,6 +9167,52 @@ "npm": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -8025,6 +9259,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/knip": { "version": "6.33.0", "resolved": "https://registry.npmjs.org/knip/-/knip-6.33.0.tgz", @@ -8132,6 +9376,16 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -8583,6 +9837,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-4.0.1.tgz", + "integrity": "sha512-wI9Nui/L8VfADa/cr/7NQruaASk1k23/Uh1khQ02BCVYiiy8F4AhOGnQzJy3Fl/c44GnYSbZHv8g7EcG3kJ1Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -8596,6 +9863,22 @@ "node": ">=10" } }, + "node_modules/macos-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/macos-version/-/macos-version-6.0.0.tgz", + "integrity": "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -8680,6 +9963,19 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/marked": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.1.tgz", + "integrity": "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8828,6 +10124,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -8836,17 +10145,126 @@ "license": "MIT", "optional": true }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/mocha": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz", + "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^5.0.0", + "debug": "^4.3.5", + "diff": "^9.0.0", + "find-up-simple": "^1.0.1", + "glob": "^13.0.0", + "is-path-inside": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^7.1.1", + "strip-json-comments": "^5.0.3", + "supports-color": "^8.1.1", + "workerpool": "^10.0.0" + }, + "bin": { + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "node_modules/mocha/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, @@ -8932,6 +10350,13 @@ "license": "MIT", "optional": true }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.54", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", @@ -8991,6 +10416,19 @@ "dev": true, "license": "ISC" }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -9073,19 +10511,21 @@ } }, "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "dev": true, "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9268,6 +10708,13 @@ "dev": true, "license": "MIT" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9299,6 +10746,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-semver": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", @@ -9551,6 +11011,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/preact": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/preact/-/preact-11.0.1.tgz", @@ -9618,6 +11091,48 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -9811,6 +11326,20 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/registry-auth-token": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", @@ -9901,6 +11430,45 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/responselike/node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -9959,6 +11527,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-jxa": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-jxa/-/run-jxa-3.0.0.tgz", + "integrity": "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "macos-version": "^6.0.0", + "subsume": "^4.0.0", + "type-fest": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-jxa/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10021,6 +11621,16 @@ "dev": true, "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -10066,6 +11676,32 @@ "node": ">=20.0.0" } }, + "node_modules/selenium-webdriver": { + "version": "4.51.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.51.0.tgz", + "integrity": "sha512-2FY/x+THHk9uWj+WLBib0r+53D1G0pidox796H5AfbRHhJdPc7CVQPNJi31ip1Tb9duBYRw0zX0wJovnbwSDHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/SeleniumHQ" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/selenium" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@bazel/runfiles": "^6.5.0", + "jszip": "^3.10.2", + "tmp": "^0.2.7", + "ws": "^8.22.0" + }, + "engines": { + "node": ">= 22.0.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -10079,6 +11715,16 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", + "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/serve": { "version": "14.2.6", "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", @@ -10200,6 +11846,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10306,6 +11972,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -10590,10 +12269,40 @@ "boundary": "^2.0.0" } }, - "node_modules/suffix-thumb": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/suffix-thumb/-/suffix-thumb-6.0.0.tgz", - "integrity": "sha512-HsCTMLaupbob5ZG//ZW6PSj446tHT5kSJ1qXS5F/qO2Nt13uAn6ZLPhJQ/H6bN/jVQCb+pcqe2psEwy4Mp5+Vw==", + "node_modules/subsume": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/subsume/-/subsume-4.0.0.tgz", + "integrity": "sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "unique-string": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/subsume/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/suffix-thumb": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/suffix-thumb/-/suffix-thumb-6.0.0.tgz", + "integrity": "sha512-HsCTMLaupbob5ZG//ZW6PSj446tHT5kSJ1qXS5F/qO2Nt13uAn6ZLPhJQ/H6bN/jVQCb+pcqe2psEwy4Mp5+Vw==", "license": "MIT", "engines": { "node": ">=18" @@ -10654,6 +12363,19 @@ "dev": true, "license": "MIT" }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/table": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", @@ -10694,6 +12416,36 @@ "node": ">=8" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -10726,6 +12478,26 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -10743,6 +12515,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10926,6 +12752,16 @@ "node": ">=20" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -11053,6 +12889,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbash": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.10.tgz", @@ -11100,6 +12949,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -11145,6 +13010,35 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/unzipper": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", + "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "11.3.1", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", @@ -11204,13 +13098,34 @@ "dev": true, "license": "MIT" }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -11455,6 +13370,270 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vscode-extension-tester": { + "version": "8.28.1", + "resolved": "https://registry.npmjs.org/vscode-extension-tester/-/vscode-extension-tester-8.28.1.tgz", + "integrity": "sha512-13Db1rHGLseNkKDKNTRHxwMOtXc3CSzUXQnRe6ZVgiNIRnDF5BEOg5RSW/fecF+7oqYWMP783szGxSHM5PueGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@redhat-developer/locators": "^1.25.1", + "@redhat-developer/page-objects": "^1.25.1", + "@types/selenium-webdriver": "^4.35.7", + "@vscode/vsce": "^4.0.0", + "c8": "^12.0.0", + "commander": "^14.0.3", + "compare-versions": "^6.1.1", + "find-up": "8.0.0", + "fs-extra": "^11.4.1", + "glob": "^13.0.6", + "got": "^16.0.0", + "hpagent": "^1.2.0", + "js-yaml": "^5.4.2", + "jsonc-parser": "^3.2.0", + "sanitize-filename": "^1.6.4", + "selenium-webdriver": "^4.49.0", + "tar": "^7.5.22", + "unzipper": "^0.12.5" + }, + "bin": { + "extest": "out/cli.js" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "mocha": ">=5.2.0", + "typescript": ">=4.6.2" + } + }, + "node_modules/vscode-extension-tester/node_modules/@vscode/vsce": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-4.0.0.tgz", + "integrity": "sha512-NImwuLaenMmb5D5Jer9/lzi/F9ZQUBOp8Azhj/BVYcTFgixv8KehFXqEUDjQlD2tAiw2E6dDGyjTuAB//di60A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.13.2", + "@napi-rs/keyring": "^1.3.0", + "@secretlint/core": "^10.2.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.2.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@vscode/vsce-sign": "^2.1.0", + "azure-devops-node-api": "^12.5.0", + "cockatiel": "^3.2.1", + "commander": "^12.1.0", + "hosted-git-info": "^4.1.0", + "jsonc-parser": "^3.3.1", + "marked": "^18.0.11", + "mime": "^1.6.0", + "minimatch": "^10.2.6", + "parse5": "^8.0.1", + "proper-lockfile": "^4.1.2", + "read": "^1.0.7", + "semver": "^7.8.5", + "tinyglobby": "^0.2.17", + "typed-rest-client": "^1.8.11", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.4.0", + "yazl": "^2.5.1" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 22" + } + }, + "node_modules/vscode-extension-tester/node_modules/@vscode/vsce/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/vscode-extension-tester/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/vscode-extension-tester/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/vscode-extension-tester/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/vscode-extension-tester/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/vscode-extension-tester/node_modules/find-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-8.0.0.tgz", + "integrity": "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^8.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vscode-extension-tester/node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/vscode-extension-tester/node_modules/locate-path": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz", + "integrity": "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vscode-extension-tester/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vscode-extension-tester/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vscode-extension-tester/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vscode-extension-tester/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/vscode-extension-tester/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.14.tgz", @@ -11632,6 +13811,69 @@ "node": ">=0.10.0" } }, + "node_modules/workerpool": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -11640,17 +13882,40 @@ "license": "ISC", "optional": true }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "dev": true, "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0" + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11773,13 +14038,12 @@ } }, "node_modules/yauzl": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", - "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "dev": true, "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", "pend": "~1.2.0" }, "engines": { @@ -11809,6 +14073,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", diff --git a/package.json b/package.json index 5407fa1c..66e2215f 100644 --- a/package.json +++ b/package.json @@ -294,7 +294,7 @@ "vscode:prepublish": "npm run build", "build": "node esbuild.mjs", "watch": "node esbuild.mjs --watch", - "lint": "eslint src/", + "lint": "eslint src/ tests/vscode/", "bench": "vitest bench", "test": "vitest run", "test:coverage": "vitest run --coverage", @@ -315,6 +315,8 @@ "check": "npm run typecheck && npm run lint && npm run spellcheck && npm run knip && npm run lockfile-lint && npm run test", "check-size": "node scripts/check-bundle-size.mjs", "test:e2e": "npx playwright test", + "compile:test:vscode": "tsc -p tests/vscode/tsconfig.build.json", + "test:vscode": "npm run compile:test:vscode && node .vscode-test/tests/run.js", "lockfile-lint": "lockfile-lint", "prepare": "husky" }, @@ -331,6 +333,7 @@ "devDependencies": { "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", + "@types/mocha": "10.0.10", "@types/node": "26.4.0", "@types/vscode": "1.125.0", "@vitest/coverage-v8": "4.1.11", @@ -346,11 +349,13 @@ "knip": "6.33.0", "lint-staged": "17.4.1", "lockfile-lint": "5.0.1", + "mocha": "12.0.3", "preact-render-to-string": "6.7.0", "serve": "14.2.6", "typescript": "6.0.3", "typescript-eslint": "8.68.0", - "vitest": "4.1.11" + "vitest": "4.1.11", + "vscode-extension-tester": "8.28.1" }, "dependencies": { "chart.js": "4.5.1", diff --git a/playwright.config.ts b/playwright.config.ts index ee6c7e01..a2b43f55 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,13 +2,22 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e', + outputDir: './test-results/browser', + forbidOnly: !!process.env.CI, + workers: process.env.CI ? 2 : undefined, + reporter: [ + ['list'], + ['html', { outputFolder: 'test-results/browser-report', open: 'never' }], + ], use: { browserName: 'chromium', headless: true, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', }, webServer: { - command: 'npx serve . -p 3999 --no-clipboard', + command: 'npx serve . --listen tcp://127.0.0.1:3999 --no-clipboard', port: 3999, - reuseExistingServer: true, + reuseExistingServer: !process.env.CI, }, }); diff --git a/tests/vscode/dashboard.test.ts b/tests/vscode/dashboard.test.ts new file mode 100644 index 00000000..35e2277f --- /dev/null +++ b/tests/vscode/dashboard.test.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { error } from 'vscode-extension-tester'; +import { Dashboard } from './dashboard'; + +const mocks = vi.hoisted(() => ({ + executeCommand: vi.fn<(title: string) => Promise>(), + defaultContent: vi.fn<() => Promise>(), + switchToFrame: vi.fn<(timeout: number) => Promise>(), + documentId: vi.fn<() => Promise>(), + editorReady: vi.fn<() => Promise>(), + takeScreenshot: vi.fn<() => Promise>(), + executeScript: vi.fn<() => Promise>(), + saveSnapshot: vi.fn(), + wait: vi.fn<(condition: () => Promise, timeout: number, message?: string) => Promise>(), +})); + +vi.mock('vscode-extension-tester', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + VSBrowser: { instance: { driver: { + switchTo: () => ({ defaultContent: mocks.defaultContent }), + findElement: () => ({ getId: mocks.documentId }), + takeScreenshot: mocks.takeScreenshot, + executeScript: mocks.executeScript, + wait: mocks.wait, + } } }, + Workbench: class { executeCommand = mocks.executeCommand; }, + WebView: class { switchToFrame = mocks.switchToFrame; }, + until: { ...actual.until, elementLocated: () => mocks.editorReady }, + }; +}); + +vi.mock('./snapshots', () => ({ saveSnapshot: mocks.saveSnapshot })); + +beforeEach(() => { + vi.resetAllMocks(); + mocks.executeCommand.mockResolvedValue(); + mocks.defaultContent.mockResolvedValue(); + mocks.switchToFrame.mockResolvedValue(); + mocks.documentId.mockResolvedValue('initial-document'); + mocks.editorReady.mockResolvedValue(true); + mocks.takeScreenshot.mockResolvedValue('captured-image'); + mocks.executeScript.mockResolvedValue('[]'); + mocks.saveSnapshot.mockResolvedValue(); + mocks.wait.mockImplementation(async (condition, _timeout, message) => { + for (let attempt = 0; attempt < 3; attempt++) { + if (await condition()) return true; + } + throw new Error(message ?? 'Wait timed out'); + }); +}); + +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); }); + +describe('desktop webview synchronization', () => { + it('opens the dashboard once and waits for its editor before entering the webview', async () => { + await new Dashboard().open(); + expect(mocks.executeCommand).toHaveBeenCalledExactlyOnceWith('AI Engineer Coach: Open Dashboard'); + expect(mocks.editorReady.mock.invocationCallOrder[0]).toBeLessThan(mocks.switchToFrame.mock.invocationCallOrder[0]); + expect(mocks.switchToFrame).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['stale frame', new error.StaleElementReferenceError('The inner frame was replaced')], + ['wrapped stale outer frame', new Error('WebviewMixin.switchToFrame: outer iframe became stale during frame switch. See the upstream issue.')], + ['missing frame', new error.NoSuchFrameError('The inner frame was removed')], + ['missing document', new error.NoSuchElementError('The document was removed')], + ])('reacquires a %s without executing the reload command again', async (_name, failure) => { + const dashboard = new Dashboard(); + await dashboard.open(); + vi.clearAllMocks(); + mocks.documentId.mockResolvedValueOnce('initial-document').mockResolvedValue('replacement-document'); + mocks.switchToFrame.mockRejectedValueOnce(failure); + + await dashboard.command('Reload Data'); + + expect(mocks.executeCommand).toHaveBeenCalledExactlyOnceWith('AI Engineer Coach: Reload Data'); + expect(mocks.switchToFrame).toHaveBeenCalledTimes(2); + const resets = mocks.defaultContent.mock.invocationCallOrder; + expect(resets.at(-1)).toBeGreaterThan(mocks.switchToFrame.mock.invocationCallOrder[0]); + expect(resets.at(-1)).toBeLessThan(mocks.switchToFrame.mock.invocationCallOrder[1]); + }); + + it('does not accept the old document while a reload is still being delivered', async () => { + const dashboard = new Dashboard(); + await dashboard.open(); + vi.clearAllMocks(); + mocks.documentId.mockResolvedValueOnce('initial-document') + .mockResolvedValueOnce('initial-document').mockResolvedValue('replacement-document'); + + await dashboard.command('Reload Data'); + + expect(mocks.switchToFrame).toHaveBeenCalledTimes(2); + expect(mocks.executeCommand).toHaveBeenCalledTimes(1); + }); + + it('uses the remaining deadline rather than restarting the timeout after a stale frame', async () => { + mocks.switchToFrame.mockRejectedValueOnce(new error.StaleElementReferenceError()); + vi.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(1000).mockReturnValue(1100); + + await new Dashboard().open(); + + expect(mocks.switchToFrame.mock.calls).toEqual([[60_000], [59_900]]); + }); +}); + +describe('desktop webview failures', () => { + it('fails if a reload never replaces the document', async () => { + const dashboard = new Dashboard(); + await dashboard.open(); + vi.clearAllMocks(); + + await expect(dashboard.command('Reload Data')).rejects.toThrow('replacement webview'); + + expect(mocks.executeCommand).toHaveBeenCalledTimes(1); + }); + + describe('desktop screenshot capture', () => { + it('saves the real driver image into this suite artifact directory without changing frame context', async () => { + vi.stubEnv('AIC_E2E_ARTIFACTS', 'isolated-artifacts'); + await new Dashboard().snapshot('01-dashboard', 'Known session counts'); + expect(mocks.takeScreenshot).toHaveBeenCalledTimes(1); + expect(mocks.saveSnapshot).toHaveBeenCalledExactlyOnceWith( + 'isolated-artifacts', { name: '01-dashboard', title: 'Known session counts' }, 'captured-image', + ); + expect(mocks.defaultContent).not.toHaveBeenCalled(); + }); + + it('fails explicitly when screenshot capture fails', async () => { + vi.stubEnv('AIC_E2E_ARTIFACTS', 'isolated-artifacts'); + mocks.takeScreenshot.mockRejectedValueOnce(new Error('Screenshot capture failed')); + await expect(new Dashboard().snapshot('01-dashboard', 'Known session counts')).rejects.toThrow('Screenshot capture failed'); + expect(mocks.saveSnapshot).not.toHaveBeenCalled(); + }); + + it('waits for changing canvas pixels to settle before taking the screenshot', async () => { + vi.stubEnv('AIC_E2E_ARTIFACTS', 'isolated-artifacts'); + mocks.executeScript.mockResolvedValueOnce('drawing').mockResolvedValue('finished'); + await new Dashboard().snapshot('01-dashboard', 'Known session counts'); + expect(mocks.executeScript).toHaveBeenCalledTimes(3); + expect(mocks.takeScreenshot.mock.invocationCallOrder[0]).toBeGreaterThan(mocks.executeScript.mock.invocationCallOrder[2]); + }); + + it('does not capture an unsettled chart as a finished milestone', async () => { + vi.stubEnv('AIC_E2E_ARTIFACTS', 'isolated-artifacts'); + mocks.executeScript.mockResolvedValueOnce('frame-1').mockResolvedValueOnce('frame-2').mockResolvedValueOnce('frame-3'); + await expect(new Dashboard().snapshot('01-dashboard', 'Known session counts')).rejects.toThrow('Waiting for charts'); + expect(mocks.takeScreenshot).not.toHaveBeenCalled(); + }); + + it('requires isolated artifact storage before capturing', async () => { + vi.stubEnv('AIC_E2E_ARTIFACTS', undefined); + await expect(new Dashboard().snapshot('01-dashboard', 'Known session counts')).rejects.toThrow('Missing isolated'); + expect(mocks.takeScreenshot).not.toHaveBeenCalled(); + }); + }); + + it('does not retry unexpected driver failures', async () => { + const failure = new Error('Driver session disconnected'); + mocks.switchToFrame.mockRejectedValueOnce(failure); + + await expect(new Dashboard().open()).rejects.toBe(failure); + + expect(mocks.switchToFrame).toHaveBeenCalledTimes(1); + }); + + it('does not retry a different ExTester frame error', async () => { + const failure = new Error('WebviewMixin.switchToFrame: active-frame iframe was not found within 60000ms.'); + mocks.switchToFrame.mockRejectedValueOnce(failure); + await expect(new Dashboard().open()).rejects.toBe(failure); + expect(mocks.switchToFrame).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tests/vscode/dashboard.ts b/tests/vscode/dashboard.ts new file mode 100644 index 00000000..3437ef4a --- /dev/null +++ b/tests/vscode/dashboard.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { By, EditorView, error, until, VSBrowser, WebView, Workbench } from 'vscode-extension-tester'; +import { saveSnapshot } from './snapshots'; + +const READY_TIMEOUT = 60_000; + +export class Dashboard { + private currentWebview: WebView | undefined; + + private get webview(): WebView { + return this.currentWebview ??= new WebView(); + } + + async command(title: 'Open Dashboard' | 'Reload Data'): Promise { + const driver = VSBrowser.instance.driver; + const previousDocumentId = title === 'Reload Data' && this.currentWebview + ? await driver.findElement(By.css('html')).getId() + : undefined; + await driver.switchTo().defaultContent(); + await new Workbench().executeCommand(`AI Engineer Coach: ${title}`); + // Page-object constructors resolve immediately; wait until the editor exists first. + await driver.wait(until.elementLocated(By.css('.editor-instance')), READY_TIMEOUT); + await this.waitForWebview(previousDocumentId); + } + + private async waitForWebview(previousDocumentId?: string): Promise { + const driver = VSBrowser.instance.driver; + const deadline = Date.now() + READY_TIMEOUT; + await driver.wait(async () => { + const remaining = deadline - Date.now(); + if (remaining <= 0) return false; + // Reload replaces the inner iframe; reacquire it from the workbench on each poll. + await driver.switchTo().defaultContent(); + try { + const webview = new WebView(); + await webview.switchToFrame(remaining); + const documentId = await driver.findElement(By.css('html')).getId(); + if (documentId === previousDocumentId) return false; + this.currentWebview = webview; + return true; + } catch (err: unknown) { + if (err instanceof error.StaleElementReferenceError || err instanceof error.NoSuchFrameError + || err instanceof error.NoSuchElementError + || (err instanceof Error && err.message.startsWith('WebviewMixin.switchToFrame: outer iframe became stale during frame switch.'))) { + return false; + } + throw err; + } + }, READY_TIMEOUT, previousDocumentId + ? 'Waiting for the replacement webview after reload' + : 'Waiting for the dashboard webview'); + } + + async open(): Promise { + await this.command('Open Dashboard'); + } + + async snapshot(name: string, title: string): Promise { + const artifacts = process.env.AIC_E2E_ARTIFACTS; + if (!artifacts) throw new Error('Missing isolated test artifact directory'); + const driver = VSBrowser.instance.driver; + let previousCharts: string | undefined; + // Canvas animations continue after the metrics and DOM become visible. + await driver.wait(async () => { + const charts = await driver.executeScript(() => + JSON.stringify(Array.from(document.querySelectorAll('canvas'), canvas => canvas.toDataURL()))); + const stable = charts === previousCharts; + previousCharts = charts; + return stable; + }, READY_TIMEOUT, 'Waiting for charts to finish rendering before the snapshot'); + await saveSnapshot(artifacts, { name, title }, await driver.takeScreenshot()); + } + + async close(): Promise { + await VSBrowser.instance.driver.switchTo().defaultContent(); + const editors = new EditorView(); + await editors.closeEditor('AI Engineer Coach'); + await VSBrowser.instance.driver.wait(async () => + !(await editors.getOpenEditorTitles()).includes('AI Engineer Coach'), READY_TIMEOUT); + this.currentWebview = undefined; + } + + async waitForText(selector: string, expected: string, exact = false): Promise { + await VSBrowser.instance.driver.wait(async () => { + try { + const elements = await this.webview.findWebElements(By.css(selector)); + for (const element of elements) { + const text = (await element.getText()).toLowerCase(); + const target = expected.toLowerCase(); + if (exact ? text === target : text.includes(target)) return true; + } + return false; + } catch (err: unknown) { + if (err instanceof error.StaleElementReferenceError || err instanceof error.NoSuchElementError) return false; + throw err; + } + }, READY_TIMEOUT, `Expected ${selector} to ${exact ? 'equal' : 'contain'} ${JSON.stringify(expected)}`); + } + + async expectCount(selector: string, expected: number): Promise { + await VSBrowser.instance.driver.wait(async () => + (await this.webview.findWebElements(By.css(selector))).length === expected, + READY_TIMEOUT, `Expected ${expected} elements matching ${selector}`); + } + + async scrollTo(selector: string): Promise { + const element = await this.webview.findWebElement(By.css(selector)); + await VSBrowser.instance.driver.executeScript((target: HTMLElement) => + target.scrollIntoView({ block: 'center', behavior: 'instant' }), element); + } + + async click(selector: string): Promise { + await VSBrowser.instance.driver.wait(async () => { + const elements = await this.webview.findWebElements(By.css(selector)); + return elements.length > 0 && await elements[0].isDisplayed(); + }, READY_TIMEOUT, `Expected visible control: ${selector}`); + await (await this.webview.findWebElement(By.css(selector))).click(); + } + + async expectStats(expected: Record): Promise { + for (const [label, value] of Object.entries(expected)) { + await VSBrowser.instance.driver.wait(async () => { + try { + for (const stat of await this.webview.findWebElements(By.css('.dash-stat'))) { + if ((await stat.findElement(By.css('.dash-stat-lbl')).getText()).toLowerCase() === label.toLowerCase()) { + return await stat.findElement(By.css('.dash-stat-val')).getText() === value; + } + } + return false; + } catch (err: unknown) { + if (err instanceof error.StaleElementReferenceError || err instanceof error.NoSuchElementError) return false; + throw err; + } + }, READY_TIMEOUT, `Expected dashboard ${label}: ${value}`); + } + await this.expectHealthy(); + } + + async navigate(page: string, heading: string): Promise { + await this.click(`[data-page="${page}"]`); + await this.waitForText('#content h1', heading, true); + } + + async expectHealthy(): Promise { + const problems = await this.webview.findWebElements(By.css('.error-boundary, .error, .loading-screen, .loading-spinner')); + for (const problem of problems) { + if (await problem.isDisplayed()) throw new Error(`Dashboard is not ready: ${await problem.getText()}`); + } + } +} diff --git a/tests/vscode/data/projects.json b/tests/vscode/data/projects.json new file mode 100644 index 00000000..266346ca --- /dev/null +++ b/tests/vscode/data/projects.json @@ -0,0 +1,134 @@ +{ + "description": "Fully synthetic projects, prompts and code. No personal session logs were used. The schedule is relative to the test anchor date.", + "schedule": [ + { "daysAgo": 0, "hour": 12, "topic": "validation" }, + { "daysAgo": 1, "hour": 9, "topic": "empty inputs" }, + { "daysAgo": 2, "hour": 14, "topic": "batch processing" }, + { "daysAgo": 3, "hour": 21, "topic": "error handling" }, + { "daysAgo": 4, "hour": 10, "topic": "regression tests" }, + { "daysAgo": 5, "hour": 16, "topic": "refactoring" }, + { "daysAgo": 6, "hour": 11, "topic": "documentation examples" } + ], + "projects": [ + { + "id": "inventory-api", + "language": "typescript", + "modelId": "gpt-4o", + "sourceFile": "inventory.ts", + "testFile": "inventory.test.ts", + "prompts": [ + "Plan how the synthetic inventory API should handle empty stock.", + "Implement the inventory availability function for this isolated example.", + "Correct the zero-stock boundary and add deterministic inventory tests." + ], + "source": [ + "export interface Item {", + " id: string;", + " quantity: number;", + "}", + "export function available(item: Item): boolean {", + " const count = item.quantity;", + " return count >= 0;", + "}" + ], + "revised": [ + "export interface Item {", + " id: string;", + " quantity: number;", + "}", + "export function available(item: Item): boolean {", + " const count = item.quantity;", + " return count > 0;", + "}" + ], + "tests": [ + "import { available } from './inventory';", + "export function testAvailability(): void {", + " if (available({ id: 'empty', quantity: 0 })) throw new Error('Expected unavailable');", + " if (!available({ id: 'stocked', quantity: 2 })) throw new Error('Expected available');", + " console.log('Synthetic inventory checks passed');", + "}" + ] + }, + { + "id": "sensor-pipeline", + "language": "python", + "modelId": "claude-sonnet-4", + "sourceFile": "readings.py", + "testFile": "test_readings.py", + "prompts": [ + "Explain a plan for normalizing synthetic sensor readings.", + "Implement the isolated reading normalization and summary functions.", + "Keep two decimal places and add deterministic reading tests." + ], + "source": [ + "def normalize_reading(value):", + " number = float(value)", + " return round(number, 1)", + "", + "def summarize_readings(values):", + " readings = [normalize_reading(value) for value in values]", + " total = sum(readings)", + " return {\"count\": len(readings), \"total\": total}" + ], + "revised": [ + "def normalize_reading(value):", + " number = float(value)", + " return round(number, 2)", + "", + "def summarize_readings(values):", + " readings = [normalize_reading(value) for value in values]", + " total = sum(readings)", + " return {\"count\": len(readings), \"total\": total}" + ], + "tests": [ + "from readings import normalize_reading, summarize_readings", + "", + "def test_readings():", + " assert normalize_reading(\"2.345\") == 2.35", + " assert summarize_readings([1, 2]) == {\"count\": 2, \"total\": 3.0}", + " assert summarize_readings([])[\"count\"] == 0" + ] + }, + { + "id": "release-worker", + "language": "go", + "modelId": "gemini-2.5-pro", + "sourceFile": "queue.go", + "testFile": "queue_test.go", + "prompts": [ + "Plan a pending-job counter for a synthetic release queue.", + "Implement the queue counter without calling external services.", + "Fix the extra pending job and add empty-queue and batch tests." + ], + "source": [ + "package queue", + "", + "func Pending(jobs []string) int {", + " count := 0", + " for range jobs { count++ }", + " if count < 0 { return 0 }", + " return count + 1", + "}" + ], + "revised": [ + "package queue", + "", + "func Pending(jobs []string) int {", + " count := 0", + " for range jobs { count++ }", + " if count < 0 { return 0 }", + " return count", + "}" + ], + "tests": [ + "package queue", + "import \"testing\"", + "func TestPending(t *testing.T) {", + " if Pending(nil) != 0 { t.Fatal(\"empty queue\") }", + " if Pending([]string{\"build\", \"test\"}) != 2 { t.Fatal(\"pending jobs\") }", + "}" + ] + } + ] +} diff --git a/tests/vscode/fixtures.test.ts b/tests/vscode/fixtures.test.ts new file mode 100644 index 00000000..55944c76 --- /dev/null +++ b/tests/vscode/fixtures.test.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + addReloadFixture, assertFixturesUnchanged, exportFixtureDataset, fixtureDatasetSchema, + fixtureFiles, fixturePaths, isolatedEnvironment, prepareFixtures, +} from './fixtures'; +import { discoverSuites, vscodeVersion } from './run'; + +const timestamp = Date.UTC(2026, 0, 15, 12); +let root: string; + +beforeEach(async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), 'aec-fixtures-')); + for (const [key, value] of Object.entries(isolatedEnvironment(root, {}))) vi.stubEnv(key, value); +}); + +afterEach(async () => { + vi.unstubAllEnvs(); + await fs.rm(root, { recursive: true, force: true }); +}); + +describe('desktop test fixtures', () => { + it('parses real raw sessions with exact counts and discovers the added reload fixture', async () => { + await prepareFixtures(root, timestamp, true); + const { findLogsDirs, parseAllLogs } = await import('../../src/core/parser'); + expect(findLogsDirs()).toEqual([fixturePaths(root).logs]); + const initial = parseAllLogs(findLogsDirs()); + expect(initial.sessions).toHaveLength(21); + expect(initial.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(63); + expect(initial.workspaces.size).toBe(3); + await assertFixturesUnchanged(root, timestamp); + await addReloadFixture(root, timestamp); + const reloaded = parseAllLogs(findLogsDirs()); + expect(reloaded.sessions).toHaveLength(22); + expect(reloaded.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(66); + await assertFixturesUnchanged(root, timestamp, true); + }); + + describe('multi-project fixture analytics', () => { + it('counts real edit operations without double-counting code repeated in responses', async () => { + await prepareFixtures(root, timestamp, true); + const { parseAllLogs } = await import('../../src/core/parser'); + const { Analyzer } = await import('../../src/core/analyzer'); + const parsed = parseAllLogs([fixturePaths(root).logs]); + const analyzer = new Analyzer(parsed.sessions, parsed.editLocIndex, parsed.workspaces); + const production = analyzer.getCodeProduction(); + expect(parsed.editLocIndex.size).toBe(42); + expect(production.summary).toMatchObject({ totalAiLoc: 315, totalRemovedAiLoc: 21, totalNetAiLoc: 294 }); + expect(production.byLanguage.labels.slice().sort()).toEqual(['go', 'python', 'typescript']); + expect(production.byLanguage.aiLoc).toEqual([105, 105, 105]); + expect(analyzer.getDailyActivity().loc).toEqual([45, 45, 45, 45, 45, 45, 45]); + await addReloadFixture(root, timestamp); + const reloaded = parseAllLogs([fixturePaths(root).logs]); + expect(new Analyzer(reloaded.sessions, reloaded.editLocIndex, reloaded.workspaces).getCodeProduction().summary) + .toMatchObject({ totalAiLoc: 330, totalRemovedAiLoc: 22, totalNetAiLoc: 308 }); + }); + + it('populates all seven days, overlapping timeline sessions, and three project summaries', async () => { + await prepareFixtures(root, timestamp, true); + const { parseAllLogs } = await import('../../src/core/parser'); + const { Analyzer } = await import('../../src/core/analyzer'); + const parsed = parseAllLogs([fixturePaths(root).logs]); + const analyzer = new Analyzer(parsed.sessions, parsed.editLocIndex, parsed.workspaces); + expect(analyzer.getHeatmap().heatmap.flat().filter(value => value > 0)).toEqual([9, 9, 9, 9, 9, 9, 9]); + expect(analyzer.getWorkLifeBalance()).toMatchObject({ totalRequests: 63, weekdayReqs: 45, weekendReqs: 18, activeDays: 7 }); + expect(analyzer.getDayTimeline()).toMatchObject({ sessionCount: 3, maxConcurrent: 3 }); + const projects = analyzer.getProjectOverview().projects; + expect(projects.map(project => project.workspaceName).sort()).toEqual(['inventory-api', 'release-worker', 'sensor-pipeline']); + expect(projects.map(project => project.totalRequests)).toEqual([21, 21, 21]); + expect(projects.map(project => project.estimatedLoc)).toEqual([105, 105, 105]); + }); + }); + + describe('published synthetic dataset', () => { + it('exports the actual initial inputs separately from the planned reload addition', async () => { + await prepareFixtures(root, timestamp, true); + const artifacts = path.join(root, 'artifacts'); + await exportFixtureDataset(root, timestamp, artifacts, true); + const summary = fixtureDatasetSchema.parse(JSON.parse(await fs.readFile(path.join(artifacts, 'dataset.json'), 'utf8'))); + expect(summary).toMatchObject({ + synthetic: true, activeDays: 7, initial: { sessions: 21, requests: 63, aiLoc: 315, netAiLoc: 294 }, + afterReload: { sessions: 22, requests: 66, aiLoc: 330, netAiLoc: 308 }, + inputFiles: { initial: 45, reload: 2 }, + }); + for (const [file, content] of fixtureFiles(root, timestamp)) { + expect(await fs.readFile(path.join(artifacts, 'fixtures', 'initial', path.relative(fixturePaths(root).logs, file)), 'utf8')) + .toBe(content); + } + await assertFixturesUnchanged(root, timestamp); + expect(await fs.readdir(path.join(artifacts, 'fixtures', 'reload', 'inventory-api', 'chatSessions'))) + .toEqual(['inventory-api-reload.json']); + }); + + it('makes the deliberately empty scenario explicit in its dataset manifest', async () => { + await prepareFixtures(root, timestamp, false); + const artifacts = path.join(root, 'artifacts'); + await exportFixtureDataset(root, timestamp, artifacts, false); + const summary = fixtureDatasetSchema.parse(JSON.parse(await fs.readFile(path.join(artifacts, 'dataset.json'), 'utf8'))); + expect(summary).toMatchObject({ projects: [], activeDays: 0, initial: { sessions: 0, requests: 0, aiLoc: 0 }, afterReload: null }); + expect(await fs.readdir(path.join(artifacts, 'fixtures'))).toEqual(['projects.json']); + }); + }); + + it('does not create a discoverable source for the empty scenario', async () => { + await prepareFixtures(root, timestamp, false); + const { findLogsDirs } = await import('../../src/core/parser'); + const { hasExternalHarnessSources } = await import('../../src/core/parser-harnesses'); + expect(findLogsDirs()).toEqual([]); + expect(hasExternalHarnessSources()).toBe(false); + }); + + it('detects session data modified by the application', async () => { + await prepareFixtures(root, timestamp, true); + const file = [...fixtureFiles(root, timestamp).keys()][0]; + await fs.appendFile(file, 'unexpected data'); + await expect(assertFixturesUnchanged(root, timestamp)).rejects.toThrow('Session-log input was modified'); + }); + + it('isolates every discovery root without mutating the parent environment', () => { + const parent = { HOME: '/personal', USERPROFILE: '/personal', COPILOT_HOME: '/personal/copilot', + VSCODE_PORTABLE: '/personal/code', VSCODE_IPC_HOOK_CLI: 'personal-socket', ELECTRON_RUN_AS_NODE: '1', + NODE_OPTIONS: '--inspect', CODE_VERSION: 'latest', PATH: '/tools', + MOCHA_GREP: 'activates|no-data', MOCHA_INVERT: 'true' }; + const env = isolatedEnvironment(root, parent); + for (const key of ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'COPILOT_HOME', 'TMP', 'TEMP', 'TMPDIR']) { + expect(env[key]?.startsWith(root + path.sep)).toBe(true); + } + expect(env).not.toHaveProperty('VSCODE_PORTABLE'); + expect(env).not.toHaveProperty('VSCODE_IPC_HOOK_CLI'); + expect(env).not.toHaveProperty('ELECTRON_RUN_AS_NODE'); + expect(env).not.toHaveProperty('NODE_OPTIONS'); + expect(env).not.toHaveProperty('CODE_VERSION'); + expect(env).not.toHaveProperty('MOCHA_GREP'); + expect(env).not.toHaveProperty('MOCHA_INVERT'); + expect(env.PATH).toBe('/tools'); + expect(parent.HOME).toBe('/personal'); + expect(parent.MOCHA_GREP).toBe('activates|no-data'); + expect(parent.MOCHA_INVERT).toBe('true'); + }); + + it('places fixture logs in the platform-specific discovery locations', () => { + expect(fixturePaths(root, 'win32').logs).toContain(path.join('AppData', 'Roaming', 'Code', 'User')); + expect(fixturePaths(root, 'darwin').logs).toContain(path.join('Library', 'Application Support', 'Code', 'User')); + expect(fixturePaths(root, 'linux').logs).toContain(path.join('.config', 'Code', 'User')); + }); +}); + +describe('desktop VS Code version selection', () => { + it('uses the manifest minimum and accepts compatible pinned or latest versions', () => { + expect(vscodeVersion('^1.125.0')).toBe('1.125.0'); + expect(vscodeVersion('^1.125.0', '1.140.0')).toBe('1.140.0'); + expect(vscodeVersion('^1.125.0', 'latest')).toBe('latest'); + }); + + describe('desktop suite discovery', () => { + it('automatically includes new source suites but never stale compiled files', async () => { + for (const file of ['populated.spec.ts', 'new-feature.spec.ts', 'helper.ts', 'deleted.spec.js']) { + await fs.writeFile(path.join(root, file), ''); + } + expect(await discoverSuites(root)).toEqual(['new-feature', 'populated']); + }); + + it('rejects an empty test selection', async () => { + await expect(discoverSuites(root)).rejects.toThrow('No desktop smoke suites found'); + }); + }); + + it('rejects incompatible versions and malformed input', () => { + for (const version of ['1.124.0', '2.0.0', '../Code', 'stable']) { + expect(() => vscodeVersion('^1.125.0', version)).toThrow(); + } + expect(() => vscodeVersion('>=1.125.0')).toThrow(); + }); +}); diff --git a/tests/vscode/fixtures.ts b/tests/vscode/fixtures.ts new file mode 100644 index 00000000..c86f4259 --- /dev/null +++ b/tests/vscode/fixtures.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { z } from 'zod'; + +const DATASET_SOURCE = path.resolve(__dirname, '..', '..', 'tests', 'vscode', 'data', 'projects.json'); +const projectSchema = z.object({ + id: z.string().regex(/^[a-z][a-z0-9-]*$/), + language: z.enum(['typescript', 'python', 'go']), + modelId: z.string(), + sourceFile: z.string().regex(/^[a-z][a-z0-9_.]*$/), + testFile: z.string().regex(/^[a-z][a-z0-9_.]*$/), + prompts: z.tuple([z.string(), z.string(), z.string()]), + source: z.array(z.string()).min(1), + revised: z.array(z.string()).min(1), + tests: z.array(z.string()).min(1), +}); +const dataset = z.object({ + description: z.string(), + schedule: z.array(z.object({ + daysAgo: z.number().int().min(0).max(6), + hour: z.number().int().min(0).max(22), + topic: z.string(), + })).min(1), + projects: z.array(projectSchema).min(1), +}).parse(JSON.parse(readFileSync(DATASET_SOURCE, 'utf8'))); +type Project = z.infer; + +export const INITIAL_COUNTS = { Requests: '63', Sessions: '21', 'AI LoC': '315', Workspaces: '3' }; +export const RELOADED_COUNTS = { Requests: '66', Sessions: '22', 'AI LoC': '330', Workspaces: '3' }; +export const INITIAL_NET_LOC = 294; +export const PROJECT_NAMES = dataset.projects.map(project => project.id); +export const FIRST_PROMPT = `${dataset.projects[0].prompts[0]} Task: ${dataset.schedule[0].topic}.`; + +const totalsSchema = z.object({ sessions: z.number(), requests: z.number(), aiLoc: z.number(), netAiLoc: z.number() }); +export const fixtureDatasetSchema = z.object({ + source: z.literal('tests/vscode/data/projects.json'), + synthetic: z.literal(true), + projects: z.array(z.object({ name: z.string(), language: z.string(), model: z.string() })), + activeDays: z.number(), + initial: totalsSchema, + afterReload: totalsSchema.nullable(), + inputFiles: z.object({ initial: z.number(), reload: z.number() }), +}); + +export function isolatedEnvironment(root: string, original: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const home = path.join(root, 'home'); + const env: NodeJS.ProcessEnv = { + ...original, + HOME: home, + USERPROFILE: home, + APPDATA: path.join(home, 'AppData', 'Roaming'), + LOCALAPPDATA: path.join(home, 'AppData', 'Local'), + XDG_CONFIG_HOME: path.join(home, '.config'), + XDG_DATA_HOME: path.join(home, '.local', 'share'), + COPILOT_HOME: path.join(home, '.copilot'), + TMPDIR: path.join(root, 'tmp'), + TMP: path.join(root, 'tmp'), + TEMP: path.join(root, 'tmp'), + }; + for (const key of Object.keys(env)) { + if (key.startsWith('VSCODE_') || key.startsWith('NODE_') + || ['ELECTRON_RUN_AS_NODE', 'EXTENSION_DEV_PATH', 'EXTENSIONS_FOLDER', 'TEST_RESOURCES', + 'CODE_VERSION', 'MOCHA_GREP', 'MOCHA_INVERT'].includes(key)) { + delete env[key]; + } + } + return env; +} + +export function fixturePaths(root: string, platform: NodeJS.Platform = process.platform) { + const home = path.join(root, 'home'); + const codeUser = platform === 'win32' + ? path.join(home, 'AppData', 'Roaming', 'Code', 'User') + : platform === 'darwin' + ? path.join(home, 'Library', 'Application Support', 'Code', 'User') + : path.join(home, '.config', 'Code', 'User'); + return { + home, + logs: path.join(codeUser, 'workspaceStorage'), + workspace: path.join(home, 'workspaces', 'smoke-workspace'), + runtimeLog: path.join(home, '.copilot-analytics-cache', 'runtime.log'), + }; +} + +function scheduledTime(anchor: number, daysAgo: number, hour: number, projectIndex: number): number { + const date = new Date(anchor); + date.setDate(date.getDate() - daysAgo); + date.setHours(hour, projectIndex * 5, 0, 0); + return date.getTime(); +} + +function addSessionFiles( + files: Map, + root: string, + project: Project, + session: { id: string; timestamp: number; topic: string }, +): void { + const { home, logs } = fixturePaths(root); + const sourceUri = pathToFileURL(path.join(home, 'workspaces', project.id, 'src', session.id, project.sourceFile)).href; + const testUri = pathToFileURL(path.join(home, 'workspaces', project.id, 'src', session.id, project.testFile)).href; + const source = project.source.join('\n') + '\n'; + const revised = project.revised.join('\n') + '\n'; + const tests = project.tests.join('\n') + '\n'; + const responses = [ + `Use a small local fixture for ${session.topic}, implement the function, then verify the boundary with tests.`, + `Implemented the synthetic example:\n\`\`\`${project.language}\n${source}\`\`\``, + `Corrected the boundary and added tests:\n\`\`\`${project.language}\n${revised}\`\`\`\n\`\`\`${project.language}\n${tests}\`\`\``, + ]; + const requests = project.prompts.map((prompt, index) => ({ + requestId: `${session.id}-request-${index}`, + timestamp: session.timestamp + [0, 7, 18][index] * 60_000, + message: { text: `${prompt} Task: ${session.topic}.` }, + response: [{ value: responses[index] }], + modelId: project.modelId, + agent: { extensionDisplayName: 'Synthetic coding agent', id: 'agent' }, + editedFileEvents: index === 0 ? [] : (index === 1 ? [sourceUri] : [sourceUri, testUri]).map(uri => ({ uri: { path: new URL(uri).pathname } })), + result: { + timings: { firstProgress: 250, totalElapsed: 2_000 + index * 500 }, + metadata: { toolCallRounds: [{ toolCalls: (index === 0 ? ['read_file'] : index === 1 + ? ['create_file'] : ['replace_string_in_file', 'create_file', 'run_in_terminal']).map(name => ({ name })) }] }, + }, + })); + files.set(path.join(logs, project.id, 'chatSessions', `${session.id}.json`), JSON.stringify({ + sessionId: session.id, customTitle: `${project.id}: ${session.topic}`, + creationDate: session.timestamp, lastMessageDate: requests[2].timestamp, requests, + }, null, 2) + '\n'); + files.set(path.join(logs, project.id, 'chatEditingSessions', session.id, 'state.json'), JSON.stringify({ + timeline: { operations: [ + { type: 'create', requestId: requests[1].requestId, uri: { external: sourceUri }, epoch: 1, initialContent: source }, + { type: 'textEdit', requestId: requests[2].requestId, uri: { external: sourceUri }, epoch: 2, + edits: [{ range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1_000_000, endColumn: 1 }, text: revised }] }, + { type: 'create', requestId: requests[2].requestId, uri: { external: testUri }, epoch: 3, initialContent: tests }, + ] }, + }, null, 2) + '\n'); +} + +export function fixtureFiles(root: string, timestamp: number, reloaded = false): Map { + const { home, logs } = fixturePaths(root); + const files = new Map(); + for (const [index, project] of dataset.projects.entries()) { + const workspace = path.join(home, 'workspaces', project.id); + files.set(path.join(logs, project.id, 'workspace.json'), JSON.stringify({ folder: pathToFileURL(workspace).href }) + '\n'); + for (const schedule of dataset.schedule) { + addSessionFiles(files, root, project, { + id: `${project.id}-day-${schedule.daysAgo}`, + timestamp: scheduledTime(timestamp, schedule.daysAgo, schedule.hour, index), + topic: schedule.topic, + }); + } + } + if (reloaded) { + addSessionFiles(files, root, dataset.projects[0], { + id: 'inventory-api-reload', timestamp: scheduledTime(timestamp, 0, 18, 0), topic: 'reload regression', + }); + } + return files; +} + +export async function prepareFixtures(root: string, timestamp: number, populated: boolean): Promise { + const { home, workspace } = fixturePaths(root); + await Promise.all([ + fs.mkdir(workspace, { recursive: true }), + fs.mkdir(path.join(root, 'tmp'), { recursive: true }), + ...PROJECT_NAMES.map(name => fs.mkdir(path.join(home, 'workspaces', name), { recursive: true })), + ]); + if (!populated) return; + for (const [file, content] of fixtureFiles(root, timestamp)) { + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, content, { flag: 'wx' }); + } +} + +async function listFiles(directory: string): Promise { + const files: string[] = []; + for (const entry of await fs.readdir(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...await listFiles(file)); + else files.push(file); + } + return files; +} + +export async function assertFixturesUnchanged(root: string, timestamp: number, reloaded = false): Promise { + const expected = fixtureFiles(root, timestamp, reloaded); + assert.deepEqual((await listFiles(fixturePaths(root).logs)).sort(), [...expected.keys()].sort(), + 'Session-log files were added or removed unexpectedly'); + for (const [file, content] of expected) { + assert.equal(await fs.readFile(file, 'utf8'), content, `Session-log input was modified: ${file}`); + } +} + +export async function addReloadFixture(root: string, timestamp: number): Promise { + await assertFixturesUnchanged(root, timestamp); + const original = fixtureFiles(root, timestamp); + for (const [file, content] of fixtureFiles(root, timestamp, true)) { + if (!original.has(file)) { + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, content, { flag: 'wx' }); + } + } +} + +export async function exportFixtureDataset(root: string, timestamp: number, artifacts: string, populated: boolean): Promise { + const initial = populated ? fixtureFiles(root, timestamp) : new Map(); + const full = populated ? fixtureFiles(root, timestamp, true) : new Map(); + for (const [file, content] of full) { + const stage = initial.has(file) ? 'initial' : 'reload'; + const target = path.join(artifacts, 'fixtures', stage, path.relative(fixturePaths(root).logs, file)); + await fs.mkdir(path.dirname(target), { recursive: true }); + if (stage === 'initial') await fs.copyFile(file, target); + else await fs.writeFile(target, content); + } + await fs.mkdir(path.join(artifacts, 'fixtures'), { recursive: true }); + await fs.copyFile(DATASET_SOURCE, path.join(artifacts, 'fixtures', 'projects.json')); + const summary = fixtureDatasetSchema.parse({ + source: 'tests/vscode/data/projects.json', synthetic: true, + projects: populated ? dataset.projects.map(project => ({ name: project.id, language: project.language, model: project.modelId })) : [], + activeDays: populated ? dataset.schedule.length : 0, + initial: populated ? { sessions: 21, requests: 63, aiLoc: 315, netAiLoc: INITIAL_NET_LOC } + : { sessions: 0, requests: 0, aiLoc: 0, netAiLoc: 0 }, + afterReload: populated ? { sessions: 22, requests: 66, aiLoc: 330, netAiLoc: 308 } : null, + inputFiles: { initial: initial.size, reload: full.size - initial.size }, + }); + await fs.writeFile(path.join(artifacts, 'dataset.json'), JSON.stringify(summary, null, 2) + '\n'); + console.log(`Synthetic dataset: ${summary.projects.length} projects, ${summary.initial.sessions} sessions, ` + + `${summary.initial.requests} requests, ${summary.initial.aiLoc} AI LoC across ${summary.activeDays} days.`); + console.log(`Fixture definitions: ${summary.source}; raw synthetic inputs: ${path.join(artifacts, 'fixtures')}`); +} + +export function scenarioContext(): { root: string; timestamp: number } { + const root = process.env.AIC_E2E_SCENARIO_ROOT; + const timestamp = Number(process.env.AIC_E2E_TIMESTAMP); + if (!root || !path.isAbsolute(root) || !Number.isSafeInteger(timestamp) || timestamp <= 0) { + throw new Error('Missing isolated test context. Run these tests through npm run test:vscode.'); + } + return { root, timestamp }; +} diff --git a/tests/vscode/run.ts b/tests/vscode/run.ts new file mode 100644 index 00000000..bc1c4b02 --- /dev/null +++ b/tests/vscode/run.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { parseArgs } from 'node:util'; +import { z } from 'zod'; +import { exportFixtureDataset, fixturePaths, isolatedEnvironment, prepareFixtures, scenarioContext } from './fixtures'; +import { writeSnapshotReport } from './snapshots'; + +const PROJECT = path.resolve(__dirname, '..', '..'); +const manifestSchema = z.object({ + name: z.string(), publisher: z.string(), version: z.string(), + engines: z.object({ vscode: z.string() }), +}); +const SUITES = path.join(PROJECT, 'tests', 'vscode', 'suite'); + +export async function discoverSuites(directory: string): Promise { + const names = (await fs.readdir(directory, { withFileTypes: true })) + .filter(entry => entry.isFile() && entry.name.endsWith('.spec.ts')) + .map(entry => entry.name.slice(0, -'.spec.ts'.length)) + .sort(); + if (names.length === 0) throw new Error('No desktop smoke suites found'); + if (names.some(name => !/^[a-z0-9][a-z0-9-]*$/.test(name))) { + throw new Error('Desktop suite filenames must use lowercase letters, numbers, and hyphens'); + } + return names; +} + +export function vscodeVersion(engine: string, override?: string): string { + if (override === 'latest') return override; + const version = override ?? /^\^(\d+\.\d+\.\d+)$/.exec(engine)?.[1]; + if (!version || !/^\d+\.\d+\.\d+$/.test(version)) { + throw new Error('Expected an exact VS Code version or "latest"; the default requires a ^x.y.z manifest engine.'); + } + const minimum = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(engine); + if (!minimum) throw new Error(`Unsupported VS Code engine range: ${engine}`); + const [major, minor, patch] = version.split('.').map(Number); + const [, minMajor, minMinor, minPatch] = minimum.map(Number); + if (major !== minMajor || minor < minMinor || (minor === minMinor && patch < minPatch)) { + throw new Error(`VS Code ${version} does not satisfy ${engine}`); + } + return version; +} + +async function execute(command: string, args: string[], env: NodeJS.ProcessEnv = process.env): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd: PROJECT, env, stdio: 'inherit' }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (signal) reject(new Error(`${path.basename(command)} terminated by ${signal}`)); + else resolve(code ?? 1); + }); + }); +} + +async function copyArchives(source: string, destination: string): Promise { + await fs.mkdir(source, { recursive: true }); + await fs.mkdir(destination, { recursive: true }); + for (const entry of await fs.readdir(source, { withFileTypes: true })) { + if (entry.isFile() && (entry.name.endsWith('.zip') || entry.name.endsWith('.tar.gz'))) { + await fs.copyFile(path.join(source, entry.name), path.join(destination, entry.name)); + } + } +} + +async function copyIfPresent(source: string, destination: string): Promise { + try { + await fs.stat(source); + } catch (err: unknown) { + if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return; + throw err; + } + await fs.cp(source, destination, { recursive: true }); +} + +async function runWorker(vsix: string, version: string, scenario: string): Promise { + if (!(await discoverSuites(SUITES)).includes(scenario)) throw new Error(`Unknown scenario: ${scenario}`); + const { root, timestamp } = scenarioContext(); + const storage = process.env.AIC_E2E_STORAGE; + const artifacts = process.env.AIC_E2E_ARTIFACTS; + if (!storage || !artifacts) throw new Error('Missing test storage/artifact directories'); + await prepareFixtures(root, timestamp, scenario !== 'empty'); + await fs.mkdir(artifacts, { recursive: true }); + await exportFixtureDataset(root, timestamp, artifacts, scenario !== 'empty'); + const config = path.join(root, 'mocha.json'); + const settings = path.join(root, 'settings.json'); + await fs.writeFile(config, JSON.stringify({ + timeout: 90_000, retries: 0, failZero: true, forbidOnly: true, forbidPending: true, reporter: 'spec', + })); + await fs.writeFile(settings, JSON.stringify({ + 'telemetry.telemetryLevel': 'off', + 'workbench.enableExperiments': false, + 'chat.disableAIFeatures': true, + 'workbench.colorTheme': 'Default Dark Modern', + })); + + // Import only in the child, after its home/profile environment has been isolated. + const { ExTester, ReleaseQuality } = await import('vscode-extension-tester'); + const extensions = path.join(root, 'extensions'); + const tester = new ExTester(storage, ReleaseQuality.Stable, extensions); + try { + await tester.downloadCode(version); + await tester.downloadChromeDriver(version); + await tester.installVsix({ vsixFile: vsix, installDependencies: false }); + const manifest = manifestSchema.parse(JSON.parse(await fs.readFile(path.join(PROJECT, 'package.json'), 'utf8'))); + const installed = (await fs.readdir(extensions, { withFileTypes: true })).filter(entry => entry.isDirectory()); + assert.equal(installed.length, 1, 'Only the packaged extension should be installed'); + const installedRoot = path.join(extensions, installed[0].name); + const installedManifest = manifestSchema.parse(JSON.parse(await fs.readFile(path.join(installedRoot, 'package.json'), 'utf8'))); + assert.deepEqual(installedManifest, manifest, 'Installed VSIX does not match the repository manifest'); + for (const asset of ['extension.js', 'parse-worker.js', 'warm-up-worker.js', 'cache-write-worker.js', + path.join('webview', 'app.js'), path.join('webview', 'styles.css')]) { + assert.ok((await fs.stat(path.join(installedRoot, 'dist', asset))).size > 0, `Missing packaged asset: ${asset}`); + } + const suite = path.join(__dirname, 'suite', `${scenario}.spec.js`).replaceAll('\\', '/'); + const code = await tester.runTests(suite, { + vscodeVersion: version, offline: true, config, settings, locale: 'en', + resources: [fixturePaths(root).workspace], + }); + if (code !== 0) throw new Error(`${scenario} VS Code smoke tests failed (${code})`); + } finally { + await copyIfPresent(fixturePaths(root).runtimeLog, path.join(artifacts, 'coach-runtime.log')); + for (const name of ['screenshots', 'chromedriver.log', 'test.log', path.join('settings', 'logs')]) { + await copyIfPresent(path.join(storage, name), path.join(artifacts, path.basename(name))); + } + } +} + +async function main(): Promise { + const { values } = parseArgs({ + options: { vsix: { type: 'string' }, 'vscode-version': { type: 'string' }, worker: { type: 'string' } }, + }); + const manifest = manifestSchema.parse(JSON.parse(await fs.readFile(path.join(PROJECT, 'package.json'), 'utf8'))); + const version = vscodeVersion(manifest.engines.vscode, values['vscode-version']); + const vsix = path.resolve(PROJECT, values.vsix ?? `${manifest.name}-${manifest.version}.vsix`); + if (values.worker) { + await runWorker(vsix, version, values.worker); + return; + } + const scenarios = await discoverSuites(SUITES); + if (!values.vsix) { + const npm = process.env.npm_execpath; + if (!npm) throw new Error('Use npm run test:vscode, or supply --vsix with an existing package.'); + assert.equal(await execute(process.execPath, [npm, 'run', 'package']), 0, 'VSIX packaging failed'); + } + const vsixHash = createHash('sha256').update(await fs.readFile(vsix)).digest('hex'); + const artifacts = path.join(PROJECT, 'test-results', 'vscode', `${Date.now()}-${process.pid}`); + await fs.mkdir(artifacts, { recursive: true }); + const runRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aec-')); + const storage = path.join(runRoot, 'runtime'); + const cache = path.join(PROJECT, '.vscode-test', 'cache', `${process.platform}-${process.arch}-${version}`); + const fixtureDate = new Date(); + fixtureDate.setDate(fixtureDate.getDate() - 1); + fixtureDate.setHours(12, 0, 0, 0); + const results: { scenario: string; exitCode: number }[] = []; + console.log(`Testing ${path.basename(vsix)} (sha256 ${vsixHash}) on VS Code ${version}`); + console.log(`Desktop diagnostics: ${artifacts}`); + try { + await copyArchives(cache, storage); + for (const scenario of scenarios) { + const root = path.join(runRoot, scenario); + const env = { + ...isolatedEnvironment(root, process.env), + AIC_E2E_SCENARIO_ROOT: root, + AIC_E2E_TIMESTAMP: String(fixtureDate.getTime()), + AIC_E2E_STORAGE: storage, + AIC_E2E_ARTIFACTS: path.join(artifacts, scenario), + }; + const exitCode = await execute(process.execPath, [__filename, '--worker', scenario, '--vsix', vsix, '--vscode-version', version], env); + results.push({ scenario, exitCode }); + } + assert.equal(createHash('sha256').update(await fs.readFile(vsix)).digest('hex'), vsixHash, 'The tested VSIX changed during the run'); + assert.ok(results.every(result => result.exitCode === 0), 'Desktop smoke tests failed; inspect the saved diagnostics.'); + } finally { + try { + const summary = { + vsix, vsixHash, version, nodeVersion: process.version, platform: process.platform, arch: process.arch, results, + }; + await fs.writeFile(path.join(artifacts, 'summary.json'), JSON.stringify(summary, null, 2)); + await writeSnapshotReport(artifacts, summary); + console.log(`Desktop screenshot gallery: ${path.join(artifacts, 'gallery.html')}`); + await copyArchives(storage, cache); + } finally { + await fs.rm(runRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }); + } + } +} + +if (require.main === module) { + void main().catch((err: unknown) => { + console.error(err); + process.exitCode = 1; + }); +} diff --git a/tests/vscode/snapshots.test.ts b/tests/vscode/snapshots.test.ts new file mode 100644 index 00000000..b4bb676b --- /dev/null +++ b/tests/vscode/snapshots.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @vitest-environment jsdom + +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { saveSnapshot, writeSnapshotReport } from './snapshots'; +import { exportFixtureDataset, prepareFixtures } from './fixtures'; + +const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jGz8AAAAASUVORK5CYII='; +const snapshot = { name: '01-dashboard', title: 'Dashboard: 2 sessions and 6 requests' }; +let root: string; + +beforeEach(async () => { root = await fs.mkdtemp(path.join(os.tmpdir(), 'aec-snapshots-')); }); +afterEach(async () => { await fs.rm(root, { recursive: true, force: true }); }); + +describe('desktop milestone snapshots', () => { + it('preserves the captured PNG and its caption without overwriting a previous milestone', async () => { + await saveSnapshot(root, snapshot, PNG); + const file = path.join(root, 'snapshots', `${snapshot.name}.png`); + expect(await fs.readFile(file)).toEqual(Buffer.from(PNG, 'base64')); + expect(JSON.parse(await fs.readFile(path.join(root, 'snapshots', `${snapshot.name}.json`), 'utf8'))).toEqual(snapshot); + await expect(saveSnapshot(root, snapshot, PNG)).rejects.toMatchObject({ code: 'EEXIST' }); + expect(await fs.readFile(file)).toEqual(Buffer.from(PNG, 'base64')); + }); + + it('rejects unsafe filenames and invalid image data', async () => { + for (const name of ['../outside', 'nested/image', 'nested\\image']) { + await expect(saveSnapshot(root, { ...snapshot, name }, PNG)).rejects.toThrow(); + } + await expect(saveSnapshot('relative', snapshot, PNG)).rejects.toThrow('absolute directory'); + await expect(saveSnapshot(root, snapshot, 'not a PNG')).rejects.toThrow('PNG image'); + expect(await fs.readdir(root)).toEqual([]); + }); +}); + +describe('desktop screenshot report', () => { + it('embeds images offline, escapes captions, and keeps failed suite outcomes visible', async () => { + const title = 'Timeline & session details'; + await saveSnapshot(path.join(root, 'populated'), { ...snapshot, title }, PNG); + await saveSnapshot(path.join(root, 'empty'), { name: '01-empty', title: 'Expected empty state' }, PNG); + await writeSnapshotReport(root, { + version: '1.125.0', platform: 'linux', vsixHash: 'abc123', + results: [{ scenario: 'empty', exitCode: 0 }, { scenario: 'populated', exitCode: 1 }], + }); + + const document = new DOMParser().parseFromString(await fs.readFile(path.join(root, 'gallery.html'), 'utf8'), 'text/html'); + const images = [...document.querySelectorAll('img')]; + expect(images).toHaveLength(2); + expect(images.map(image => image.getAttribute('src'))).toEqual([`data:image/png;base64,${PNG}`, `data:image/png;base64,${PNG}`]); + expect(images[1].alt).toBe(title); + expect(document.querySelectorAll('script')).toHaveLength(0); + expect(document.body.textContent).toContain('populated: Failed; 1 snapshot'); + expect(document.body.textContent).toContain('abc123'); + expect(document.body.textContent).toContain('1.125.0'); + expect(document.querySelector('meta[http-equiv="Content-Security-Policy"]')?.getAttribute('content')).toContain("default-src 'none'"); + const summary = await fs.readFile(path.join(root, 'snapshots.md'), 'utf8'); + expect(summary).toContain('2 milestone screenshots captured'); + expect(summary).toContain('populated: Failed'); + }); + + it('reports a suite that fails before taking any screenshots without inventing success', async () => { + await writeSnapshotReport(root, { + version: 'latest', platform: 'linux', vsixHash: 'abc123', + results: [{ scenario: 'populated', exitCode: 1 }], + }); + expect(await fs.readFile(path.join(root, 'gallery.html'), 'utf8')).toContain('No snapshots were captured'); + expect(await fs.readFile(path.join(root, 'snapshots.md'), 'utf8')).toContain('Failed; 0 snapshots'); + }); + + it('makes the checked-in dataset and generated raw inputs visible in the gallery and CI summary', async () => { + const scenarioRoot = path.join(root, 'scenario'); + const timestamp = Date.UTC(2026, 0, 15, 12); + await prepareFixtures(scenarioRoot, timestamp, true); + await exportFixtureDataset(scenarioRoot, timestamp, path.join(root, 'populated'), true); + await writeSnapshotReport(root, { + version: '1.125.0', platform: 'linux', vsixHash: 'abc123', + results: [{ scenario: 'populated', exitCode: 0 }], + }); + for (const name of ['gallery.html', 'snapshots.md']) { + const report = await fs.readFile(path.join(root, name), 'utf8'); + expect(report).toContain('3 projects, 21 sessions, 63 requests, 315 AI LoC'); + expect(report).toContain('inventory-api (typescript)'); + expect(report).toContain('sensor-pipeline (python)'); + expect(report).toContain('release-worker (go)'); + expect(report).toContain('tests/vscode/data/projects.json'); + expect(report).toContain('populated/fixtures/initial'); + expect(report).toContain('no personal logs were read'); + } + }); + + it('surfaces a missing captured PNG rather than generating a broken image', async () => { + const suite = path.join(root, 'populated'); + await saveSnapshot(suite, snapshot, PNG); + await fs.unlink(path.join(suite, 'snapshots', `${snapshot.name}.png`)); + await expect(writeSnapshotReport(root, { + version: '1.125.0', platform: 'linux', vsixHash: 'abc123', + results: [{ scenario: 'populated', exitCode: 0 }], + })).rejects.toMatchObject({ code: 'ENOENT' }); + }); +}); diff --git a/tests/vscode/snapshots.ts b/tests/vscode/snapshots.ts new file mode 100644 index 00000000..be772e85 --- /dev/null +++ b/tests/vscode/snapshots.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { z } from 'zod'; +import { fixtureDatasetSchema } from './fixtures'; + +const safeName = z.string().regex(/^[a-z0-9][a-z0-9-]*$/); +const snapshotSchema = z.object({ name: safeName, title: z.string().min(1) }); +const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); +type Snapshot = z.infer; +interface GallerySnapshot extends Snapshot { scenario: string; image: string } +interface SnapshotRun { + version: string; + platform: string; + vsixHash: string; + results: { scenario: string; exitCode: number }[]; +} + +export async function saveSnapshot(artifacts: string, snapshot: Snapshot, image: string): Promise { + if (!path.isAbsolute(artifacts)) throw new Error('Snapshot artifacts must use an absolute directory'); + const metadata = snapshotSchema.parse(snapshot); + const png = Buffer.from(image, 'base64'); + if (!png.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) { + throw new Error('Desktop snapshot did not contain a PNG image'); + } + const directory = path.join(artifacts, 'snapshots'); + await fs.mkdir(directory, { recursive: true }); + await fs.writeFile(path.join(directory, `${metadata.name}.png`), png, { flag: 'wx' }); + await fs.writeFile(path.join(directory, `${metadata.name}.json`), JSON.stringify(metadata) + '\n', { flag: 'wx' }); +} + +async function readSnapshots(artifacts: string, scenario: string): Promise { + const directory = path.join(artifacts, safeName.parse(scenario), 'snapshots'); + let entries: string[]; + try { + entries = await fs.readdir(directory); + } catch (err: unknown) { + // A suite can fail before reaching its first snapshot. + if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return []; + throw err; + } + const snapshots: GallerySnapshot[] = []; + for (const file of entries.filter(entry => entry.endsWith('.json')).sort()) { + const metadata = snapshotSchema.parse(JSON.parse(await fs.readFile(path.join(directory, file), 'utf8'))); + const png = await fs.readFile(path.join(directory, `${metadata.name}.png`)); + snapshots.push({ ...metadata, scenario, image: `data:image/png;base64,${png.toString('base64')}` }); + } + return snapshots; +} + +function escapeHtml(value: string): string { + return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') + .replaceAll('"', '"').replaceAll("'", '''); +} + +async function datasetHtml(artifacts: string, scenario: string): Promise { + const file = path.join(artifacts, safeName.parse(scenario), 'dataset.json'); + let content: string; + try { + content = await fs.readFile(file, 'utf8'); + } catch (err: unknown) { + if (err instanceof Error && 'code' in err && err.code === 'ENOENT') return ''; + throw err; + } + const dataset = fixtureDatasetSchema.parse(JSON.parse(content)); + const projects = dataset.projects.map(project => + `${escapeHtml(project.name)} (${escapeHtml(project.language)})`).join(', '); + return `
  • ${escapeHtml(scenario)}: ${dataset.projects.length} projects, ` + + `${dataset.initial.sessions} sessions, ${dataset.initial.requests} requests, ${dataset.initial.aiLoc} AI LoC ` + + `(${dataset.initial.netAiLoc} net) across ${dataset.activeDays} active days.` + + `${projects ? `
    Projects: ${projects}.` : '
    Deliberately empty profile.'}` + + `${dataset.afterReload ? `
    After the planned reload addition: ${dataset.afterReload.sessions} sessions, ` + + `${dataset.afterReload.requests} requests, ${dataset.afterReload.aiLoc} AI LoC (${dataset.afterReload.netAiLoc} net).` : ''}` + + (dataset.inputFiles.initial > 0 + ? `
    Raw inputs: ${escapeHtml(scenario)}/fixtures/initial; ` + + `planned reload files: ${escapeHtml(scenario)}/fixtures/reload.` + : '
    No session input files are seeded for this scenario.') + '
  • '; +} + +function galleryHtml(run: SnapshotRun, snapshots: GallerySnapshot[], outcomes: string, datasets: string): string { + return ` + + + + + +VS Code smoke snapshots + + + +

    VS Code smoke snapshots

    +

    VS Code target: ${escapeHtml(run.version)} on ${escapeHtml(run.platform)}.

    +

    Tested VSIX SHA-256: ${escapeHtml(run.vsixHash)}

    +

    Captured after milestone assertions against synthetic data. These are execution evidence, not pixel-comparison baselines. +A later failure still makes the suite fail.

    +

    Suite results

    +
      ${outcomes}
    +${datasets} +${snapshots.length === 0 ? '

    No snapshots were captured. Inspect the test logs for startup or assertion failures.

    ' + : snapshots.map(snapshot => `
    +
    ${escapeHtml(snapshot.title)}
    ${escapeHtml(snapshot.scenario)} / ${escapeHtml(snapshot.name)}
    +${escapeHtml(snapshot.title)} +
    `).join('\n')} + + +`; +} + +export async function writeSnapshotReport(artifacts: string, run: SnapshotRun): Promise { + const snapshots = (await Promise.all(run.results.map(result => readSnapshots(artifacts, result.scenario)))).flat(); + const outcomes = run.results.map(result => { + const count = snapshots.filter(snapshot => snapshot.scenario === result.scenario).length; + return `
  • ${escapeHtml(result.scenario)}: ${result.exitCode === 0 ? 'Passed' : 'Failed'}; ${count} snapshot${count === 1 ? '' : 's'}
  • `; + }).join('\n') || '
  • No suites completed.
  • '; + const datasetRows = (await Promise.all(run.results.map(result => datasetHtml(artifacts, result.scenario)))).join('\n'); + const datasets = datasetRows.trim() ? `

    Synthetic test data

    +

    Fully synthetic; no personal logs were read. Definitions: tests/vscode/data/projects.json. +Raw input copies and the planned reload addition are included in the artifact, alongside each suite's dataset.json.

    +
      ${datasetRows}
    ` : ''; + await fs.writeFile(path.join(artifacts, 'gallery.html'), galleryHtml(run, snapshots, outcomes, datasets)); + await fs.writeFile(path.join(artifacts, 'snapshots.md'), `## VS Code smoke snapshots + +VS Code target: ${escapeHtml(run.version)} on ${escapeHtml(run.platform)}. + +
      ${outcomes}
    + +${datasets} + +${snapshots.length} milestone screenshots captured. Suite results above include failures after a capture. +`); +} diff --git a/tests/vscode/suite/empty.spec.ts b/tests/vscode/suite/empty.spec.ts new file mode 100644 index 00000000..2c285940 --- /dev/null +++ b/tests/vscode/suite/empty.spec.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; +import { describe, it } from 'mocha'; +import { Dashboard } from '../dashboard'; +import { fixturePaths, scenarioContext } from '../fixtures'; + +describe('Packaged VSIX without session logs', () => { + it('shows the no-data explanation instead of hanging or reading another profile', async () => { + const dashboard = new Dashboard(); + await dashboard.open(); + await dashboard.waitForText('.error p', 'No AI coding session logs found.'); + const runtime = await fs.readFile(fixturePaths(scenarioContext().root).runtimeLog, 'utf8'); + assert.match(runtime, /logs-dirs-found[^\n]*count=0 external=false/); + assert.match(runtime, /loadData-no-dirs/); + assert.doesNotMatch(runtime, /parse-complete|uncaught-exception|unhandled-rejection/); + await dashboard.snapshot('01-empty-state', 'Empty profile: the expected no-session-data explanation'); + }); +}); diff --git a/tests/vscode/suite/populated.spec.ts b/tests/vscode/suite/populated.spec.ts new file mode 100644 index 00000000..a2084ee6 --- /dev/null +++ b/tests/vscode/suite/populated.spec.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; +import { after, before, describe, it } from 'mocha'; +import { VSBrowser } from 'vscode-extension-tester'; +import { Dashboard } from '../dashboard'; +import { + addReloadFixture, assertFixturesUnchanged, FIRST_PROMPT, fixturePaths, + INITIAL_COUNTS, INITIAL_NET_LOC, PROJECT_NAMES, RELOADED_COUNTS, scenarioContext, +} from '../fixtures'; + +describe('Packaged VSIX with synthetic sessions', () => { + const dashboard = new Dashboard(); + const { root, timestamp } = scenarioContext(); + let reloaded = false; + + before(async () => { + await dashboard.open(); + }); + + after(async () => { + await assertFixturesUnchanged(root, timestamp, reloaded); + const runtime = await fs.readFile(fixturePaths(root).runtimeLog, 'utf8'); + assert.match(runtime, /\[extension\] activate/); + assert.match(runtime, /\[panel\] parse-complete/); + assert.match(runtime, /\[panel\] warmUp-done/); + assert.doesNotMatch(runtime, /uncaught-exception|unhandled-rejection|loadData-error|rules-load-error|metrics-load-error/); + }); + + it('activates the installed extension and renders real worker results', async () => { + await dashboard.expectStats(INITIAL_COUNTS); + await dashboard.waitForText('#content', 'Local Agent'); + await dashboard.snapshot('01-dashboard', 'Dashboard: 3 projects, 21 sessions, 63 requests and 315 AI LoC'); + await dashboard.scrollTo('#dailyChart'); + await dashboard.snapshot('01-dashboard-activity', 'Dashboard activity: 7 days and the three-project breakdown'); + await dashboard.scrollTo('#content'); + }); + + it('navigates to the timeline and displays a synthetic session', async () => { + await dashboard.navigate('timeline', 'Timeline'); + await dashboard.waitForText('#timelineStats', '3 sessions'); + await dashboard.waitForText('#timelineStats', '9 requests'); + await dashboard.waitForText('#timelineStats', 'Max concurrent: 3'); + await dashboard.click('.timeline-lane'); + await dashboard.waitForText('#timelineDetail', FIRST_PROMPT); + await dashboard.expectHealthy(); + await dashboard.snapshot('02-timeline', 'Timeline: 3 overlapping projects and the selected session details'); + }); + + it('loads the Output page through the real webview message bridge', async () => { + await dashboard.navigate('output', 'Output'); + await dashboard.waitForText('#content', 'AI-Generated LoC'); + await dashboard.click('#outputRange [data-range="28"]'); + await dashboard.waitForText('#output-tab-content .stat-card:nth-child(1) .stat-value', INITIAL_COUNTS['AI LoC'], true); + await dashboard.waitForText('#output-tab-content .stat-card:nth-child(2) .stat-value', String(INITIAL_NET_LOC), true); + await dashboard.click('[data-prod-tab="workspace"]'); + await dashboard.expectHealthy(); + await dashboard.snapshot('03-output', 'Output by project: 315 added lines and 294 net lines across 7 days'); + }); + + it('renders nonempty activity patterns and the three-project breakdown', async () => { + await dashboard.navigate('patterns', 'Activity Patterns'); + await dashboard.expectCount('#heatmapGrid .heatmap-cell', 168); + await dashboard.expectCount('#heatmapGrid .heatmap-cell:not(:empty)', 7); + await dashboard.waitForText('#heatmapGrid .heatmap-cell', '9', true); + await dashboard.expectHealthy(); + await dashboard.snapshot('04-patterns-hours', 'Patterns: 7 active days with weekday, weekend and evening activity'); + await dashboard.click('#patternTabs [data-tab="projects"]'); + await dashboard.expectCount('#tabProjects .project-card', 3); + for (const project of PROJECT_NAMES) await dashboard.waitForText('#tabProjects .proj-name', project, true); + await dashboard.waitForText('#tabProjects .proj-requests', '21 reqs', true); + await dashboard.waitForText('#tabProjects .proj-loc', '105 LoC', true); + await dashboard.expectHealthy(); + await dashboard.snapshot('05-patterns-projects', 'Patterns: TypeScript, Python and Go project summaries'); + }); + + it('reloads changed files without retaining stale results or duplicating sessions', async () => { + await addReloadFixture(root, timestamp); + reloaded = true; + await dashboard.command('Reload Data'); + // A replacement webview starts on Dashboard; its layout can still be loading. + await dashboard.expectStats(RELOADED_COUNTS); + await dashboard.expectCount('#sidebar [data-page="dashboard"].active', 1); + const log = fixturePaths(root).runtimeLog; + const parsesBefore = (await fs.readFile(log, 'utf8')).match(/\[panel\] parse-complete/g)?.length ?? 0; + await dashboard.command('Reload Data'); + await VSBrowser.instance.driver.wait(async () => + ((await fs.readFile(log, 'utf8')).match(/\[panel\] parse-complete/g)?.length ?? 0) > parsesBefore, + 60_000, 'Reload should run the parse worker again'); + await dashboard.expectStats(RELOADED_COUNTS); + const runtime = await fs.readFile(log, 'utf8'); + assert.match(runtime, /\[extension\] command-reload/); + assert.match(runtime, /\[panel\] parse-complete[^\n]*sessions=22/); + await dashboard.snapshot('06-reloaded', 'Reloaded twice: 22 sessions, 66 requests and 330 AI LoC, without duplicates'); + }); + + it('reopens the dashboard with the same data', async () => { + await dashboard.close(); + await dashboard.open(); + await dashboard.expectStats(RELOADED_COUNTS); + await dashboard.snapshot('07-reopened', 'Reopened dashboard: the same 22 sessions, 66 requests and 330 AI LoC'); + }); +}); diff --git a/tests/vscode/tsconfig.build.json b/tests/vscode/tsconfig.build.json new file mode 100644 index 00000000..f4575ba8 --- /dev/null +++ b/tests/vscode/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "rootDir": ".", + "outDir": "../../.vscode-test/tests", + "noEmit": false + }, + "include": ["run.ts", "fixtures.ts", "dashboard.ts", "suite/**/*.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index b3c20792..b89a19c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,6 @@ "noEmit": true, "types": ["node"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "tests/vscode/**/*.ts"], "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.mts b/vitest.config.mts index b11f9bc4..4f79d61b 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -7,7 +7,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['src/**/*.test.ts'], + include: ['src/**/*.test.ts', 'tests/vscode/**/*.test.ts'], environment: 'node', testTimeout: 15_000, coverage: {