From a9c261e9bdb0e7cd5d1e0eafab0d63b0eeeaafa7 Mon Sep 17 00:00:00 2001 From: Sanjay Singh Date: Thu, 22 Oct 2026 07:08:20 +0200 Subject: [PATCH 1/5] test: add packaged VS Code end-to-end smoke tests Add isolated synthetic-session fixtures and real VSIX desktop smoke coverage. Run browser and desktop suites in PR/release pipelines and schedule cross-platform compatibility checks. Resolves #208 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/e2e/action.yml | 64 + .github/workflows/ci.yml | 18 +- .github/workflows/release.yml | 21 +- .github/workflows/vscode-compatibility.yml | 47 + .vscodeignore | 2 + CONTRIBUTING.md | 87 + eslint.config.mjs | 4 +- knip.json | 6 +- package-lock.json | 2535 +++++++++++++++++++- package.json | 9 +- playwright.config.ts | 13 +- tests/vscode/dashboard.ts | 94 + tests/vscode/fixtures.test.ts | 110 + tests/vscode/fixtures.ts | 139 ++ tests/vscode/run.ts | 196 ++ tests/vscode/suite/empty.spec.ts | 22 + tests/vscode/suite/populated.spec.ts | 77 + tests/vscode/tsconfig.build.json | 12 + tsconfig.json | 2 +- vitest.config.mts | 2 +- 20 files changed, 3309 insertions(+), 151 deletions(-) create mode 100644 .github/actions/e2e/action.yml create mode 100644 .github/workflows/vscode-compatibility.yml create mode 100644 tests/vscode/dashboard.ts create mode 100644 tests/vscode/fixtures.test.ts create mode 100644 tests/vscode/fixtures.ts create mode 100644 tests/vscode/run.ts create mode 100644 tests/vscode/suite/empty.spec.ts create mode 100644 tests/vscode/suite/populated.spec.ts create mode 100644 tests/vscode/tsconfig.build.json diff --git a/.github/actions/e2e/action.yml b/.github/actions/e2e/action.yml new file mode 100644 index 00000000..46d5a4c1 --- /dev/null +++ b/.github/actions/e2e/action.yml @@ -0,0 +1,64 @@ +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 + 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 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..515038b4 --- /dev/null +++ b/.github/workflows/vscode-compatibility.yml @@ -0,0 +1,47 @@ +name: VS Code compatibility + +on: + workflow_dispatch: + 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..0125a53f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,93 @@ 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 and on manual dispatch. + +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 + +[`tests/vscode/fixtures.ts`](tests/vscode/fixtures.ts) creates raw VS Code session JSON in the +platform's normal discovery layout, entirely inside a fresh temporary home. Two workspaces +initially contain two sessions and six requests. The reload test adds one synthetic session, +then verifies three sessions and nine requests, including a second reload without duplicates. +The empty-data scenario starts a separate process with a fresh home and no session sources. + +The data structure and prompts are deterministic; timestamps are anchored to noon on the +previous day so date-based views do not age out. The unit suite checks the raw data through +the real parser. 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. + +### 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. +The desktop suite is sequential, has no automatic retries, and rejects focused, skipped, or +empty test runs. +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.ts b/tests/vscode/dashboard.ts new file mode 100644 index 00000000..f0e84167 --- /dev/null +++ b/tests/vscode/dashboard.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +const READY_TIMEOUT = 60_000; + +export class Dashboard { + private currentWebview: WebView | undefined; + + private get webview(): WebView { + return this.currentWebview ??= new WebView(); + } + + async command(title: string): Promise { + await VSBrowser.instance.driver.switchTo().defaultContent(); + await new Workbench().executeCommand(`AI Engineer Coach: ${title}`); + // Page-object constructors resolve immediately; wait until the editor exists first. + await VSBrowser.instance.driver.wait(until.elementLocated(By.css('.editor-instance')), READY_TIMEOUT); + this.currentWebview = new WebView(); + await this.webview.switchToFrame(READY_TIMEOUT); + } + + async open(): Promise { + await this.command('Open Dashboard'); + } + + 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 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/fixtures.test.ts b/tests/vscode/fixtures.test.ts new file mode 100644 index 00000000..26cd80c8 --- /dev/null +++ b/tests/vscode/fixtures.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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, 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(2); + expect(initial.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(6); + expect(initial.workspaces.size).toBe(2); + await assertFixturesUnchanged(root, timestamp); + await addReloadFixture(root, timestamp); + const reloaded = parseAllLogs(findLogsDirs()); + expect(reloaded.sessions).toHaveLength(3); + expect(reloaded.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(9); + await assertFixturesUnchanged(root, timestamp, true); + }); + + 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' }; + 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.PATH).toBe('/tools'); + expect(parent.HOME).toBe('/personal'); + }); + + 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..a4c2a171 --- /dev/null +++ b/tests/vscode/fixtures.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export const INITIAL_COUNTS = { Requests: '6', Sessions: '2', Workspaces: '2' }; +export const RELOADED_COUNTS = { Requests: '9', Sessions: '3', Workspaces: '2' }; +export const FIRST_PROMPT = 'Explain how the synthetic API handles a missing item.'; + +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'].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 sessionJson(id: string, timestamp: number, firstPrompt: string): string { + const prompts = [firstPrompt, 'Add a deterministic test for the missing item.', 'Explain the test result.']; + return JSON.stringify({ + sessionId: id, + customTitle: id, + creationDate: timestamp, + lastMessageDate: timestamp + 120_000, + requests: prompts.map((text, index) => ({ + requestId: `${id}-request-${index}`, + timestamp: timestamp + index * 60_000, + message: { text }, + response: [{ value: `Synthetic answer ${index + 1}: the example is handled without external services.` }], + modelId: 'gpt-4o', + result: { timings: { firstProgress: 100, totalElapsed: 500 }, metadata: {} }, + })), + }, 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, name] of ['smoke-api', 'smoke-ui'].entries()) { + const workspace = path.join(home, 'workspaces', name); + files.set(path.join(logs, name, 'workspace.json'), JSON.stringify({ folder: pathToFileURL(workspace).href }) + '\n'); + files.set( + path.join(logs, name, 'chatSessions', `${name}.json`), + sessionJson(name, timestamp + index * 600_000, index === 0 ? FIRST_PROMPT : 'Explain the synthetic dashboard.'), + ); + } + if (reloaded) { + files.set(path.join(logs, 'smoke-api', 'chatSessions', 'smoke-reload.json'), + sessionJson('smoke-reload', timestamp + 1_200_000, 'Verify that reloading discovers this new synthetic session.')); + } + 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 }), + fs.mkdir(path.join(home, 'workspaces', 'smoke-api'), { recursive: true }), + fs.mkdir(path.join(home, 'workspaces', 'smoke-ui'), { 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.writeFile(file, content, { flag: 'wx' }); + } +} + +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..851e41a0 --- /dev/null +++ b/tests/vscode/run.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { fixturePaths, isolatedEnvironment, prepareFixtures, scenarioContext } from './fixtures'; + +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 }); + 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 { + await fs.writeFile(path.join(artifacts, 'summary.json'), JSON.stringify({ + vsix, vsixHash, version, nodeVersion: process.version, platform: process.platform, arch: process.arch, results, + }, null, 2)); + 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/suite/empty.spec.ts b/tests/vscode/suite/empty.spec.ts new file mode 100644 index 00000000..248f42de --- /dev/null +++ b/tests/vscode/suite/empty.spec.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * 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/); + }); +}); diff --git a/tests/vscode/suite/populated.spec.ts b/tests/vscode/suite/populated.spec.ts new file mode 100644 index 00000000..bdb50fdd --- /dev/null +++ b/tests/vscode/suite/populated.spec.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * 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, 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'); + }); + + it('navigates to the timeline and displays a synthetic session', async () => { + await dashboard.navigate('timeline', 'Timeline'); + await dashboard.waitForText('#timelineStats', '2 sessions'); + await dashboard.waitForText('#timelineStats', '6 requests'); + await dashboard.click('.timeline-lane'); + await dashboard.waitForText('#timelineDetail', FIRST_PROMPT); + await dashboard.expectHealthy(); + }); + + 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.expectHealthy(); + }); + + it('reloads changed files without retaining stale results or duplicating sessions', async () => { + await addReloadFixture(root, timestamp); + reloaded = true; + await dashboard.command('Reload Data'); + await dashboard.click('[data-page="dashboard"]'); + await dashboard.expectStats(RELOADED_COUNTS); + 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=3/); + }); + + it('reopens the dashboard with the same data', async () => { + await dashboard.close(); + await dashboard.open(); + await dashboard.expectStats(RELOADED_COUNTS); + }); +}); 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: { From 1114a94192c568bf19a1794dc58cd26df7047f80 Mon Sep 17 00:00:00 2001 From: Sanjay Singh Date: Fri, 23 Oct 2026 09:50:20 +0200 Subject: [PATCH 2/5] test: synchronize VS Code webview replacement on reload Wait for the replacement document and reacquire transiently stale frames within one deadline. Keep reload execution and smoke assertions unchanged, with deterministic regression coverage for the Linux CI race. Fixes CI for #302; relates to #208. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 6 +- tests/vscode/dashboard.test.ts | 121 +++++++++++++++++++++++++++++++++ tests/vscode/dashboard.ts | 38 +++++++++-- 3 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 tests/vscode/dashboard.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0125a53f..eef223c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,8 +106,10 @@ each file gets a separate process, profile, and baseline synthetic data. `empty. 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. -The desktop suite is sequential, has no automatic retries, and rejects focused, skipped, or -empty test runs. +The desktop suite is sequential, has no automatic test retries, and rejects focused, skipped, or +empty test runs. Reload replaces the webview's inner frame, so the dashboard helper waits for a +different document and reacquires detached frames within a single deadline. 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, diff --git a/tests/vscode/dashboard.test.ts b/tests/vscode/dashboard.test.ts new file mode 100644 index 00000000..09d8c494 --- /dev/null +++ b/tests/vscode/dashboard.test.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * 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>(), + 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 }), + wait: mocks.wait, + } } }, + Workbench: class { executeCommand = mocks.executeCommand; }, + WebView: class { switchToFrame = mocks.switchToFrame; }, + until: { ...actual.until, elementLocated: () => mocks.editorReady }, + }; +}); + +beforeEach(() => { + vi.resetAllMocks(); + mocks.executeCommand.mockResolvedValue(); + mocks.defaultContent.mockResolvedValue(); + mocks.switchToFrame.mockResolvedValue(); + mocks.documentId.mockResolvedValue('initial-document'); + mocks.editorReady.mockResolvedValue(true); + 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(); }); + +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')], + ['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); + }); + + 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); + }); +}); diff --git a/tests/vscode/dashboard.ts b/tests/vscode/dashboard.ts index f0e84167..9823a39a 100644 --- a/tests/vscode/dashboard.ts +++ b/tests/vscode/dashboard.ts @@ -14,13 +14,41 @@ export class Dashboard { return this.currentWebview ??= new WebView(); } - async command(title: string): Promise { - await VSBrowser.instance.driver.switchTo().defaultContent(); + 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 VSBrowser.instance.driver.wait(until.elementLocated(By.css('.editor-instance')), READY_TIMEOUT); - this.currentWebview = new WebView(); - await this.webview.switchToFrame(READY_TIMEOUT); + 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) return false; + throw err; + } + }, READY_TIMEOUT, previousDocumentId + ? 'Waiting for the replacement webview after reload' + : 'Waiting for the dashboard webview'); } async open(): Promise { From 07b2ce0efe83e3de61f78a292ee2d37a6a8d55df Mon Sep 17 00:00:00 2001 From: Sanjay Singh Date: Fri, 23 Oct 2026 11:23:51 +0200 Subject: [PATCH 3/5] test: publish successful VS Code smoke snapshots Capture six named synthetic-data milestones after assertions and chart rendering. Generate an offline screenshot gallery and link its diagnostics artifact from the GitHub Actions summary, preserving failure outcomes and existing failure screenshots. Relates to #208 and #302. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/e2e/action.yml | 20 +++++ CONTRIBUTING.md | 15 ++++ tests/vscode/dashboard.test.ts | 52 ++++++++++++- tests/vscode/dashboard.ts | 17 +++++ tests/vscode/run.ts | 8 +- tests/vscode/snapshots.test.ts | 84 +++++++++++++++++++++ tests/vscode/snapshots.ts | 107 +++++++++++++++++++++++++++ tests/vscode/suite/empty.spec.ts | 1 + tests/vscode/suite/populated.spec.ts | 5 ++ 9 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 tests/vscode/snapshots.test.ts create mode 100644 tests/vscode/snapshots.ts diff --git a/.github/actions/e2e/action.yml b/.github/actions/e2e/action.yml index 46d5a4c1..f4834692 100644 --- a/.github/actions/e2e/action.yml +++ b/.github/actions/e2e/action.yml @@ -55,6 +55,7 @@ runs: fi - name: Upload test diagnostics + id: diagnostics if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: @@ -62,3 +63,22 @@ runs: 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/CONTRIBUTING.md b/CONTRIBUTING.md index eef223c2..f94d76e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,6 +93,19 @@ screenshots, and a package-hash summary. Browser traces, screenshots, and the HT are stored separately under `test-results/browser` and `test-results/browser-report`. GitHub Actions uploads these diagnostics even when a test fails. +Successful milestones also capture real VS Code screenshots: the initial dashboard, Timeline +session details, Output, 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, 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 @@ -106,6 +119,8 @@ each file gets a separate process, profile, and baseline synthetic data. `empty. 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. Reload replaces the webview's inner frame, so the dashboard helper waits for a different document and reacquires detached frames within a single deadline. It does not repeat diff --git a/tests/vscode/dashboard.test.ts b/tests/vscode/dashboard.test.ts index 09d8c494..24bd4306 100644 --- a/tests/vscode/dashboard.test.ts +++ b/tests/vscode/dashboard.test.ts @@ -13,6 +13,9 @@ const mocks = vi.hoisted(() => ({ 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>(), })); @@ -23,6 +26,8 @@ vi.mock('vscode-extension-tester', async importOriginal => { 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; }, @@ -31,6 +36,8 @@ vi.mock('vscode-extension-tester', async importOriginal => { }; }); +vi.mock('./snapshots', () => ({ saveSnapshot: mocks.saveSnapshot })); + beforeEach(() => { vi.resetAllMocks(); mocks.executeCommand.mockResolvedValue(); @@ -38,6 +45,9 @@ beforeEach(() => { 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; @@ -46,7 +56,7 @@ beforeEach(() => { }); }); -afterEach(() => { vi.restoreAllMocks(); }); +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); }); describe('desktop webview synchronization', () => { it('opens the dashboard once and waits for its editor before entering the webview', async () => { @@ -110,6 +120,46 @@ describe('desktop webview failures', () => { 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); diff --git a/tests/vscode/dashboard.ts b/tests/vscode/dashboard.ts index 9823a39a..2b69132e 100644 --- a/tests/vscode/dashboard.ts +++ b/tests/vscode/dashboard.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { By, EditorView, error, until, VSBrowser, WebView, Workbench } from 'vscode-extension-tester'; +import { saveSnapshot } from './snapshots'; const READY_TIMEOUT = 60_000; @@ -55,6 +56,22 @@ export class Dashboard { 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(); diff --git a/tests/vscode/run.ts b/tests/vscode/run.ts index 851e41a0..71d5e548 100644 --- a/tests/vscode/run.ts +++ b/tests/vscode/run.ts @@ -12,6 +12,7 @@ import * as path from 'node:path'; import { parseArgs } from 'node:util'; import { z } from 'zod'; import { fixturePaths, isolatedEnvironment, prepareFixtures, scenarioContext } from './fixtures'; +import { writeSnapshotReport } from './snapshots'; const PROJECT = path.resolve(__dirname, '..', '..'); const manifestSchema = z.object({ @@ -178,9 +179,12 @@ async function main(): Promise { assert.ok(results.every(result => result.exitCode === 0), 'Desktop smoke tests failed; inspect the saved diagnostics.'); } finally { try { - await fs.writeFile(path.join(artifacts, 'summary.json'), JSON.stringify({ + const summary = { vsix, vsixHash, version, nodeVersion: process.version, platform: process.platform, arch: process.arch, results, - }, null, 2)); + }; + 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 }); diff --git a/tests/vscode/snapshots.test.ts b/tests/vscode/snapshots.test.ts new file mode 100644 index 00000000..ed0c5b12 --- /dev/null +++ b/tests/vscode/snapshots.test.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +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('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..ebf59731 --- /dev/null +++ b/tests/vscode/snapshots.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 fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { z } from 'zod'; + +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("'", '''); +} + +function galleryHtml(run: SnapshotRun, snapshots: GallerySnapshot[], outcomes: 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}
+${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.
  • '; + await fs.writeFile(path.join(artifacts, 'gallery.html'), galleryHtml(run, snapshots, outcomes)); + await fs.writeFile(path.join(artifacts, 'snapshots.md'), `## VS Code smoke snapshots + +VS Code target: ${escapeHtml(run.version)} on ${escapeHtml(run.platform)}. + +
      ${outcomes}
    + +${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 index 248f42de..2c285940 100644 --- a/tests/vscode/suite/empty.spec.ts +++ b/tests/vscode/suite/empty.spec.ts @@ -18,5 +18,6 @@ describe('Packaged VSIX without session logs', () => { 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 index bdb50fdd..b86ad908 100644 --- a/tests/vscode/suite/populated.spec.ts +++ b/tests/vscode/suite/populated.spec.ts @@ -34,6 +34,7 @@ describe('Packaged VSIX with synthetic sessions', () => { 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: 2 sessions and 6 requests from synthetic logs'); }); it('navigates to the timeline and displays a synthetic session', async () => { @@ -43,12 +44,14 @@ describe('Packaged VSIX with synthetic sessions', () => { await dashboard.click('.timeline-lane'); await dashboard.waitForText('#timelineDetail', FIRST_PROMPT); await dashboard.expectHealthy(); + await dashboard.snapshot('02-timeline', 'Timeline: synthetic 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.expectHealthy(); + await dashboard.snapshot('03-output', 'Output: analysis through the real webview message bridge'); }); it('reloads changed files without retaining stale results or duplicating sessions', async () => { @@ -67,11 +70,13 @@ describe('Packaged VSIX with synthetic sessions', () => { const runtime = await fs.readFile(log, 'utf8'); assert.match(runtime, /\[extension\] command-reload/); assert.match(runtime, /\[panel\] parse-complete[^\n]*sessions=3/); + await dashboard.snapshot('04-reloaded', 'Reloaded twice: 3 sessions and 9 requests, 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('05-reopened', 'Reopened dashboard: the same 3 sessions and 9 requests'); }); }); From 32c353242ac1df2a94ba88652915c8c9b0e88ca6 Mon Sep 17 00:00:00 2001 From: Sanjay Singh Date: Fri, 23 Oct 2026 11:41:47 +0200 Subject: [PATCH 4/5] test: add data-rich multi-project desktop fixtures Check in fictional TypeScript, Python and Go projects with seven days of raw sessions and edit timelines. Verify meaningful Dashboard, Timeline, Output and Patterns results, publish the synthetic input files and dataset manifest, and capture nine UI milestones. No personal logs were read or copied. Relates to #208 and #302. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 41 +++++-- tests/vscode/dashboard.ts | 12 ++ tests/vscode/data/projects.json | 134 +++++++++++++++++++++ tests/vscode/fixtures.test.ts | 78 ++++++++++++- tests/vscode/fixtures.ts | 167 ++++++++++++++++++++++----- tests/vscode/run.ts | 3 +- tests/vscode/snapshots.test.ts | 22 ++++ tests/vscode/snapshots.ts | 37 +++++- tests/vscode/suite/populated.spec.ts | 42 +++++-- 9 files changed, 475 insertions(+), 61 deletions(-) create mode 100644 tests/vscode/data/projects.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f94d76e3..7d521d19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,15 +73,25 @@ requirement to accommodate the runner. Dependency updates use the existing Depen ### Synthetic data and isolation -[`tests/vscode/fixtures.ts`](tests/vscode/fixtures.ts) creates raw VS Code session JSON in the -platform's normal discovery layout, entirely inside a fresh temporary home. Two workspaces -initially contain two sessions and six requests. The reload test adds one synthetic session, -then verifies three sessions and nine requests, including a second reload without duplicates. -The empty-data scenario starts a separate process with a fresh home and no session sources. - -The data structure and prompts are deterministic; timestamps are anchored to noon on the -previous day so date-based views do not age out. The unit suite checks the raw data through -the real parser. Desktop tests do not replace the parser, workers, VS Code API, or message bridge. +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. @@ -93,11 +103,18 @@ screenshots, and a package-hash summary. Browser traces, screenshots, and the HT are stored separately under `test-results/browser` and `test-results/browser-report`. GitHub Actions uploads these diagnostics even when a test fails. -Successful milestones also capture real VS Code screenshots: the initial dashboard, Timeline -session details, Output, reload, reopening, and the expected empty-data state. In a GitHub Actions +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, the editor target, and the tested VSIX hash. +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`. diff --git a/tests/vscode/dashboard.ts b/tests/vscode/dashboard.ts index 2b69132e..7d7e10af 100644 --- a/tests/vscode/dashboard.ts +++ b/tests/vscode/dashboard.ts @@ -98,6 +98,18 @@ export class Dashboard { }, 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)); 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 index 26cd80c8..22c25e18 100644 --- a/tests/vscode/fixtures.test.ts +++ b/tests/vscode/fixtures.test.ts @@ -8,7 +8,8 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { - addReloadFixture, assertFixturesUnchanged, fixtureFiles, fixturePaths, isolatedEnvironment, prepareFixtures, + addReloadFixture, assertFixturesUnchanged, exportFixtureDataset, fixtureDatasetSchema, + fixtureFiles, fixturePaths, isolatedEnvironment, prepareFixtures, } from './fixtures'; import { discoverSuites, vscodeVersion } from './run'; @@ -31,17 +32,82 @@ describe('desktop test fixtures', () => { const { findLogsDirs, parseAllLogs } = await import('../../src/core/parser'); expect(findLogsDirs()).toEqual([fixturePaths(root).logs]); const initial = parseAllLogs(findLogsDirs()); - expect(initial.sessions).toHaveLength(2); - expect(initial.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(6); - expect(initial.workspaces.size).toBe(2); + 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(3); - expect(reloaded.sessions.reduce((sum, session) => sum + session.requests.length, 0)).toBe(9); + 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'); diff --git a/tests/vscode/fixtures.ts b/tests/vscode/fixtures.ts index a4c2a171..2da22a87 100644 --- a/tests/vscode/fixtures.ts +++ b/tests/vscode/fixtures.ts @@ -4,13 +4,51 @@ *--------------------------------------------------------------------------------------------*/ 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'; -export const INITIAL_COUNTS = { Requests: '6', Sessions: '2', Workspaces: '2' }; -export const RELOADED_COUNTS = { Requests: '9', Sessions: '3', Workspaces: '2' }; -export const FIRST_PROMPT = 'Explain how the synthetic API handles a missing item.'; +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'); @@ -51,38 +89,76 @@ export function fixturePaths(root: string, platform: NodeJS.Platform = process.p }; } -function sessionJson(id: string, timestamp: number, firstPrompt: string): string { - const prompts = [firstPrompt, 'Add a deterministic test for the missing item.', 'Explain the test result.']; - return JSON.stringify({ - sessionId: id, - customTitle: id, - creationDate: timestamp, - lastMessageDate: timestamp + 120_000, - requests: prompts.map((text, index) => ({ - requestId: `${id}-request-${index}`, - timestamp: timestamp + index * 60_000, - message: { text }, - response: [{ value: `Synthetic answer ${index + 1}: the example is handled without external services.` }], - modelId: 'gpt-4o', - result: { timings: { firstProgress: 100, totalElapsed: 500 }, metadata: {} }, - })), - }, null, 2) + '\n'; +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, name] of ['smoke-api', 'smoke-ui'].entries()) { - const workspace = path.join(home, 'workspaces', name); - files.set(path.join(logs, name, 'workspace.json'), JSON.stringify({ folder: pathToFileURL(workspace).href }) + '\n'); - files.set( - path.join(logs, name, 'chatSessions', `${name}.json`), - sessionJson(name, timestamp + index * 600_000, index === 0 ? FIRST_PROMPT : 'Explain the synthetic dashboard.'), - ); + 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) { - files.set(path.join(logs, 'smoke-api', 'chatSessions', 'smoke-reload.json'), - sessionJson('smoke-reload', timestamp + 1_200_000, 'Verify that reloading discovers this new synthetic session.')); + addSessionFiles(files, root, dataset.projects[0], { + id: 'inventory-api-reload', timestamp: scheduledTime(timestamp, 0, 18, 0), topic: 'reload regression', + }); } return files; } @@ -92,8 +168,7 @@ export async function prepareFixtures(root: string, timestamp: number, populated await Promise.all([ fs.mkdir(workspace, { recursive: true }), fs.mkdir(path.join(root, 'tmp'), { recursive: true }), - fs.mkdir(path.join(home, 'workspaces', 'smoke-api'), { recursive: true }), - fs.mkdir(path.join(home, 'workspaces', 'smoke-ui'), { 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)) { @@ -125,8 +200,38 @@ 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.writeFile(file, content, { flag: 'wx' }); + 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 } { diff --git a/tests/vscode/run.ts b/tests/vscode/run.ts index 71d5e548..bc1c4b02 100644 --- a/tests/vscode/run.ts +++ b/tests/vscode/run.ts @@ -11,7 +11,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { parseArgs } from 'node:util'; import { z } from 'zod'; -import { fixturePaths, isolatedEnvironment, prepareFixtures, scenarioContext } from './fixtures'; +import { exportFixtureDataset, fixturePaths, isolatedEnvironment, prepareFixtures, scenarioContext } from './fixtures'; import { writeSnapshotReport } from './snapshots'; const PROJECT = path.resolve(__dirname, '..', '..'); @@ -88,6 +88,7 @@ async function runWorker(vsix: string, version: string, scenario: string): Promi 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({ diff --git a/tests/vscode/snapshots.test.ts b/tests/vscode/snapshots.test.ts index ed0c5b12..b4bb676b 100644 --- a/tests/vscode/snapshots.test.ts +++ b/tests/vscode/snapshots.test.ts @@ -10,6 +10,7 @@ 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' }; @@ -72,6 +73,27 @@ describe('desktop screenshot report', () => { 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); diff --git a/tests/vscode/snapshots.ts b/tests/vscode/snapshots.ts index ebf59731..be772e85 100644 --- a/tests/vscode/snapshots.ts +++ b/tests/vscode/snapshots.ts @@ -6,6 +6,7 @@ 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) }); @@ -56,7 +57,31 @@ function escapeHtml(value: string): string { .replaceAll('"', '"').replaceAll("'", '''); } -function galleryHtml(run: SnapshotRun, snapshots: GallerySnapshot[], outcomes: string): string { +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 ` @@ -79,6 +104,7 @@ figcaption { margin-bottom: 1rem; } img { display: block; width: 100%; height: a 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)}
    @@ -95,13 +121,20 @@ export async function writeSnapshotReport(artifacts: string, run: SnapshotRun): 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.
  • '; - await fs.writeFile(path.join(artifacts, 'gallery.html'), galleryHtml(run, snapshots, outcomes)); + 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/populated.spec.ts b/tests/vscode/suite/populated.spec.ts index b86ad908..7f0b18b7 100644 --- a/tests/vscode/suite/populated.spec.ts +++ b/tests/vscode/suite/populated.spec.ts @@ -10,7 +10,7 @@ import { VSBrowser } from 'vscode-extension-tester'; import { Dashboard } from '../dashboard'; import { addReloadFixture, assertFixturesUnchanged, FIRST_PROMPT, fixturePaths, - INITIAL_COUNTS, RELOADED_COUNTS, scenarioContext, + INITIAL_COUNTS, INITIAL_NET_LOC, PROJECT_NAMES, RELOADED_COUNTS, scenarioContext, } from '../fixtures'; describe('Packaged VSIX with synthetic sessions', () => { @@ -34,24 +34,48 @@ describe('Packaged VSIX with synthetic sessions', () => { 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: 2 sessions and 6 requests from synthetic logs'); + 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', '2 sessions'); - await dashboard.waitForText('#timelineStats', '6 requests'); + 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: synthetic session details'); + 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: analysis through the real webview message bridge'); + 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 () => { @@ -69,14 +93,14 @@ describe('Packaged VSIX with synthetic sessions', () => { 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=3/); - await dashboard.snapshot('04-reloaded', 'Reloaded twice: 3 sessions and 9 requests, without duplicates'); + 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('05-reopened', 'Reopened dashboard: the same 3 sessions and 9 requests'); + await dashboard.snapshot('07-reopened', 'Reopened dashboard: the same 22 sessions, 66 requests and 330 AI LoC'); }); }); From b58e734d903f019fc928e9aa6c97f3032f0bb0a6 Mon Sep 17 00:00:00 2001 From: Sanjay Singh Date: Fri, 23 Oct 2026 11:54:17 +0200 Subject: [PATCH 5/5] test: address desktop harness review feedback Handle ExTester's precise stale-outer-frame wrapper, remove inherited Mocha selection filters, and wait for the default dashboard after reload. Add regression coverage and run compatibility checks for desktop-harness PR changes. Addresses review feedback on #302; relates to #208. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/vscode-compatibility.yml | 5 +++++ CONTRIBUTING.md | 11 +++++++---- tests/vscode/dashboard.test.ts | 8 ++++++++ tests/vscode/dashboard.ts | 5 ++++- tests/vscode/fixtures.test.ts | 7 ++++++- tests/vscode/fixtures.ts | 3 ++- tests/vscode/suite/populated.spec.ts | 3 ++- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/vscode-compatibility.yml b/.github/workflows/vscode-compatibility.yml index 515038b4..c3dd228b 100644 --- a/.github/workflows/vscode-compatibility.yml +++ b/.github/workflows/vscode-compatibility.yml @@ -2,6 +2,11 @@ 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' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d521d19..7a7864b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,8 @@ By default, desktop tests use the minimum version in `package.json`'s `engines.v 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 and on manual dispatch. +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 @@ -139,9 +140,11 @@ 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. Reload replaces the webview's inner frame, so the dashboard helper waits for a -different document and reacquires detached frames within a single deadline. It does not repeat -the reload command or suppress unexpected driver failures. +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, diff --git a/tests/vscode/dashboard.test.ts b/tests/vscode/dashboard.test.ts index 24bd4306..35e2277f 100644 --- a/tests/vscode/dashboard.test.ts +++ b/tests/vscode/dashboard.test.ts @@ -68,6 +68,7 @@ describe('desktop webview synchronization', () => { 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) => { @@ -168,4 +169,11 @@ describe('desktop webview failures', () => { 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 index 7d7e10af..3437ef4a 100644 --- a/tests/vscode/dashboard.ts +++ b/tests/vscode/dashboard.ts @@ -44,7 +44,10 @@ export class Dashboard { return true; } catch (err: unknown) { if (err instanceof error.StaleElementReferenceError || err instanceof error.NoSuchFrameError - || err instanceof error.NoSuchElementError) return false; + || 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 diff --git a/tests/vscode/fixtures.test.ts b/tests/vscode/fixtures.test.ts index 22c25e18..55944c76 100644 --- a/tests/vscode/fixtures.test.ts +++ b/tests/vscode/fixtures.test.ts @@ -126,7 +126,8 @@ describe('desktop test fixtures', () => { 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' }; + 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); @@ -136,8 +137,12 @@ describe('desktop test fixtures', () => { 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', () => { diff --git a/tests/vscode/fixtures.ts b/tests/vscode/fixtures.ts index 2da22a87..c86f4259 100644 --- a/tests/vscode/fixtures.ts +++ b/tests/vscode/fixtures.ts @@ -67,7 +67,8 @@ export function isolatedEnvironment(root: string, original: NodeJS.ProcessEnv): }; 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'].includes(key)) { + || ['ELECTRON_RUN_AS_NODE', 'EXTENSION_DEV_PATH', 'EXTENSIONS_FOLDER', 'TEST_RESOURCES', + 'CODE_VERSION', 'MOCHA_GREP', 'MOCHA_INVERT'].includes(key)) { delete env[key]; } } diff --git a/tests/vscode/suite/populated.spec.ts b/tests/vscode/suite/populated.spec.ts index 7f0b18b7..a2084ee6 100644 --- a/tests/vscode/suite/populated.spec.ts +++ b/tests/vscode/suite/populated.spec.ts @@ -82,8 +82,9 @@ describe('Packaged VSIX with synthetic sessions', () => { await addReloadFixture(root, timestamp); reloaded = true; await dashboard.command('Reload Data'); - await dashboard.click('[data-page="dashboard"]'); + // 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');