From 63b2e1c4ccd88629143591c09f90ed9a7246b1a8 Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 02:56:00 -0500 Subject: [PATCH 01/13] ci: E2E workflow, web typecheck job, pre-commit hook, test suite CI: - ci.yml consolidated to reference ci-tests.yml - ci-quality.yml: add typecheck-web job for gitnexus-web/ - ci-e2e.yml: E2E workflow with dorny/paths-filter (web changes only) - ci-report.yml: remove dead integration-reports references - CI gate allows skipped E2E status - .gitignore: playwright artifacts, eval test artifacts Pre-commit hook: - .githooks/pre-commit: typecheck + unit tests for both packages - Activated via git config core.hooksPath in prepare script Test infrastructure: - Vitest + React Testing Library: 58 unit tests (graph, server-connection, mermaid, settings, constants, utils, paths) - Playwright E2E: 5 tests + manual recording harness - vitest.config from vitest/config, engines.node >= 20 - Playwright artifacts retain-on-failure - wait-on in devDependencies - vitest/coverage-v8 aligned with vitest 4.x Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci-e2e.yml | 87 +++ .github/workflows/ci-quality.yml | 15 + .github/workflows/ci-report.yml | 541 ++++++++++-------- .github/workflows/ci.yml | 83 +-- .gitignore | 10 + gitnexus-web/e2e/debug-issues.spec.ts | 126 ++++ gitnexus-web/e2e/manual-record.spec.ts | 28 + gitnexus-web/e2e/server-connect.spec.ts | 170 ++++++ gitnexus-web/package.json | 29 +- gitnexus-web/playwright.config.ts | 48 ++ gitnexus-web/test/fixtures/graph.ts | 66 +++ gitnexus-web/test/setup.ts | 6 +- gitnexus-web/test/unit/constants.test.ts | 81 +++ gitnexus-web/test/unit/graph.test.ts | 68 +++ .../test/unit/mermaid-generator.test.ts | 107 ++++ .../test/unit/path-resolution.test.ts | 31 + .../test/unit/server-connection.test.ts | 76 +++ .../test/unit/settings-service.test.ts | 145 +++++ gitnexus-web/test/unit/utils.test.ts | 17 + gitnexus-web/vitest.config.ts | 37 +- gitnexus/vitest.config.ts | 2 +- 21 files changed, 1477 insertions(+), 296 deletions(-) create mode 100644 .github/workflows/ci-e2e.yml create mode 100644 gitnexus-web/e2e/debug-issues.spec.ts create mode 100644 gitnexus-web/e2e/manual-record.spec.ts create mode 100644 gitnexus-web/e2e/server-connect.spec.ts create mode 100644 gitnexus-web/playwright.config.ts create mode 100644 gitnexus-web/test/fixtures/graph.ts create mode 100644 gitnexus-web/test/unit/constants.test.ts create mode 100644 gitnexus-web/test/unit/graph.test.ts create mode 100644 gitnexus-web/test/unit/mermaid-generator.test.ts create mode 100644 gitnexus-web/test/unit/path-resolution.test.ts create mode 100644 gitnexus-web/test/unit/server-connection.test.ts create mode 100644 gitnexus-web/test/unit/settings-service.test.ts create mode 100644 gitnexus-web/test/unit/utils.test.ts diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml new file mode 100644 index 0000000000..7793f74986 --- /dev/null +++ b/.github/workflows/ci-e2e.yml @@ -0,0 +1,87 @@ +name: E2E Tests + +on: + workflow_call: + +jobs: + check-changes: + name: Check web module changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + web_changed: ${{ steps.filter.outputs.web }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 + id: filter + with: + filters: | + web: + - 'gitnexus-web/**' + + e2e: + name: e2e (chromium) + needs: check-changes + if: needs.check-changes.result == 'success' && needs.check-changes.outputs.web_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: gitnexus-web/package-lock.json + + - name: Install frontend dependencies + run: npm ci + working-directory: gitnexus-web + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + working-directory: gitnexus-web + + - name: Install backend dependencies + run: npm ci + working-directory: gitnexus + + - name: Build backend + run: npm run build + working-directory: gitnexus + + - name: Analyze repository (index for backend) + run: node dist/cli/index.js analyze + working-directory: gitnexus + + - name: Start backend server + run: node dist/cli/index.js serve & + working-directory: gitnexus + + - name: Wait for backend readiness + run: npx wait-on http://localhost:4747/api/repos --timeout 30000 + working-directory: gitnexus-web + + - name: Start Vite dev server + run: npm run dev & + working-directory: gitnexus-web + + - name: Wait for Vite dev server + run: npx wait-on http://localhost:5173 --timeout 30000 + working-directory: gitnexus-web + + - name: Run E2E tests + run: npx playwright test + working-directory: gitnexus-web + env: + E2E: '1' + + - name: Upload test results + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: e2e-results + path: | + gitnexus-web/test-results/ + gitnexus-web/playwright-report/ + retention-days: 5 diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml index d20e824ab0..d5ddbf230b 100644 --- a/.github/workflows/ci-quality.yml +++ b/.github/workflows/ci-quality.yml @@ -12,3 +12,18 @@ jobs: - uses: ./.github/actions/setup-gitnexus - run: npx tsc --noEmit working-directory: gitnexus + + typecheck-web: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: gitnexus-web/package-lock.json + - run: npm ci + working-directory: gitnexus-web + - run: npx tsc -b --noEmit + working-directory: gitnexus-web diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index 1f1b70cb6d..4d4628e69f 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -1,132 +1,126 @@ name: CI Report +# Triggered after the CI workflow completes. Because workflow_run +# always runs code from the *default branch*, it receives a read/write +# GITHUB_TOKEN — even when the triggering PR comes from a fork. + on: workflow_run: - workflows: ['CI'] + workflows: ["CI"] types: [completed] permissions: - actions: read - contents: read - pull-requests: write + actions: read # needed to list/download workflow run artifacts + contents: read # needed for sparse checkout of vitest.config.ts + pull-requests: write # needed to post sticky PR comment jobs: pr-report: name: PR Report + # Only run for pull-request CI runs if: >- github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion != 'cancelled' runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Download PR metadata + # ── Download artifacts from the CI run ──────────────────────── + - name: Download artifacts uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: script: | const fs = require('fs'); const path = require('path'); + const runId = context.payload.workflow_run.id; - const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: ${{ github.event.workflow_run.id }}, + run_id: runId, }); - const meta = artifacts.data.artifacts.find(a => a.name === 'pr-meta'); - if (!meta) { - core.setFailed('pr-meta artifact not found — skipping report'); - return; + async function downloadArtifact(name, dest) { + const match = allArtifacts.data.artifacts.find(a => a.name === name); + if (!match) { + core.warning(`Artifact "${name}" not found`); + return false; + } + const zip = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: match.id, + archive_format: 'zip', + }); + fs.mkdirSync(dest, { recursive: true }); + fs.writeFileSync(path.join(dest, `${name}.zip`), Buffer.from(zip.data)); + return true; } - const zip = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: meta.id, - archive_format: 'zip', - }); - - const dest = path.join(process.env.RUNNER_TEMP, 'pr-meta'); - fs.mkdirSync(dest, { recursive: true }); - fs.writeFileSync(path.join(dest, 'pr-meta.zip'), Buffer.from(zip.data)); + const temp = process.env.RUNNER_TEMP; + await downloadArtifact('pr-meta', path.join(temp, 'dl')); + await downloadArtifact('test-reports', path.join(temp, 'dl')); - - name: Extract PR metadata + - name: Extract artifacts + shell: bash + run: | + cd "$RUNNER_TEMP/dl" + # Extract each artifact into its own directory to avoid filename collisions + for z in *.zip; do + [ -f "$z" ] || continue + name="${z%.zip}" + mkdir -p "$RUNNER_TEMP/artifacts/$name" + unzip -o "$z" -d "$RUNNER_TEMP/artifacts/$name" + done + + - name: Read PR metadata id: meta shell: bash run: | - cd "$RUNNER_TEMP/pr-meta" - unzip -o pr-meta.zip - - PR_NUMBER=$(cat pr-number | tr -d '[:space:]') - if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then - echo "::error::Invalid PR number: '$PR_NUMBER'" - exit 1 + DIR="$RUNNER_TEMP/artifacts/pr-meta" + if [ ! -f "$DIR/pr_number" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "::warning::pr_number artifact missing — skipping report" + exit 0 fi - echo "pr-number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - echo "quality=$(cat quality-result | tr -d '[:space:]')" >> "$GITHUB_OUTPUT" - echo "tests=$(cat tests-result | tr -d '[:space:]')" >> "$GITHUB_OUTPUT" - - - name: Download test reports - id: download-test-reports - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 - with: - script: | - const fs = require('fs'); - const path = require('path'); - - const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{ github.event.workflow_run.id }}, - }); - - const reports = artifacts.data.artifacts.find(a => a.name === 'test-reports'); - if (!reports) { - core.warning('test-reports artifact not found'); - return; - } - - const zip = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: reports.id, - archive_format: 'zip', - }); - - const dest = path.join(process.env.RUNNER_TEMP, 'test-reports'); - fs.mkdirSync(dest, { recursive: true }); - fs.writeFileSync(path.join(dest, 'test-reports.zip'), Buffer.from(zip.data)); - - - name: Extract test reports - if: steps.download-test-reports.outcome == 'success' - shell: bash - run: | - cd "$RUNNER_TEMP/test-reports" - unzip -o test-reports.zip || true + # Validate PR number is a positive integer (artifact comes from + # untrusted fork code, so treat contents defensively). + PR_NUM=$(cat "$DIR/pr_number" | tr -d '[:space:]') + if ! [[ "$PR_NUM" =~ ^[0-9]+$ ]]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "::error::Invalid PR number in artifact: '$PR_NUM'" + exit 0 + fi - - name: Fetch cross-platform job results - id: jobs - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT" + # Validate job-result strings against known GitHub Actions values. + # Artifact contents come from the PR workflow (potentially untrusted + # fork code), so we whitelist to prevent newline injection into + # GITHUB_OUTPUT. + validate_result() { + local val + val=$(cat "$1" | tr -d '[:space:]') + case "$val" in + success|failure|cancelled|skipped) echo "$val" ;; + *) echo "unknown" ;; + esac + } + + echo "quality=$(validate_result "$DIR/quality_result")" >> "$GITHUB_OUTPUT" + echo "tests=$(validate_result "$DIR/tests_result")" >> "$GITHUB_OUTPUT" + echo "e2e=$(validate_result "$DIR/e2e_result")" >> "$GITHUB_OUTPUT" + + - name: Checkout (for vitest config) + if: steps.meta.outputs.skip != 'true' + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - script: | - const jobs = await github.rest.actions.listJobsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{ github.event.workflow_run.id }}, - per_page: 50, - }); - - const results = {}; - for (const job of jobs.data.jobs) { - if (job.name.includes('ubuntu')) results.ubuntu = job.conclusion || 'pending'; - else if (job.name.includes('windows')) results.windows = job.conclusion || 'pending'; - else if (job.name.includes('macos')) results.macos = job.conclusion || 'pending'; - } - core.setOutput('ubuntu', results.ubuntu || 'unknown'); - core.setOutput('windows', results.windows || 'unknown'); - core.setOutput('macos', results.macos || 'unknown'); + sparse-checkout: gitnexus/vitest.config.ts + sparse-checkout-cone-mode: false + # ── Fetch base branch coverage for delta reporting ─────────── - name: Fetch base branch coverage + if: steps.meta.outputs.skip != 'true' id: base-coverage uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 with: @@ -134,6 +128,7 @@ jobs: const fs = require('fs'); const path = require('path'); + // Find the latest successful CI run on main const runs = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, @@ -145,6 +140,7 @@ jobs: if (runs.data.workflow_runs.length === 0) { core.setOutput('found', 'false'); + core.info('No successful main branch CI runs found'); return; } @@ -158,6 +154,7 @@ jobs: const testReports = artifacts.data.artifacts.find(a => a.name === 'test-reports'); if (!testReports) { core.setOutput('found', 'false'); + core.info('No test-reports artifact on main branch'); return; } @@ -175,174 +172,224 @@ jobs: core.setOutput('dir', dest); - name: Extract base coverage - if: steps.base-coverage.outputs.found == 'true' + if: steps.meta.outputs.skip != 'true' && steps.base-coverage.outputs.found == 'true' shell: bash run: | cd "${{ steps.base-coverage.outputs.dir }}" unzip -o base.zip -d base - - name: Build and post report - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + - name: Build report + if: steps.meta.outputs.skip != 'true' + id: report + shell: bash env: - PR_NUMBER: ${{ steps.meta.outputs.pr-number }} QUALITY: ${{ steps.meta.outputs.quality }} TESTS: ${{ steps.meta.outputs.tests }} - UBUNTU: ${{ steps.jobs.outputs.ubuntu }} - WINDOWS: ${{ steps.jobs.outputs.windows }} - MACOS: ${{ steps.jobs.outputs.macos }} + E2E: ${{ steps.meta.outputs.e2e }} BASE_FOUND: ${{ steps.base-coverage.outputs.found }} BASE_DIR: ${{ steps.base-coverage.outputs.dir }} - RUN_ID: ${{ github.event.workflow_run.id }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - with: - script: | - const fs = require('fs'); - const path = require('path'); - - const icon = (s) => ({ success: '✅', failure: '❌', cancelled: '⏭️' }[s] || '❓'); - const temp = process.env.RUNNER_TEMP; - - // ── Read coverage ── - function readCov(dir) { - const out = { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A', - stmtsCov: '', branchCov: '', funcsCov: '', linesCov: '' }; - try { - const files = require('child_process') - .execSync(`find "${dir}" -name coverage-summary.json -type f`, { encoding: 'utf8' }) - .trim().split('\n').filter(Boolean); - if (!files.length) return out; - const d = JSON.parse(fs.readFileSync(files[0], 'utf8')).total; - out.stmts = d.statements.pct; out.branch = d.branches.pct; - out.funcs = d.functions.pct; out.lines = d.lines.pct; - out.stmtsCov = `${d.statements.covered}/${d.statements.total}`; - out.branchCov = `${d.branches.covered}/${d.branches.total}`; - out.funcsCov = `${d.functions.covered}/${d.functions.total}`; - out.linesCov = `${d.lines.covered}/${d.lines.total}`; - } catch {} - return out; - } - - const cov = readCov(path.join(temp, 'test-reports')); - const base = process.env.BASE_FOUND === 'true' - ? readCov(path.join(process.env.BASE_DIR, 'base')) - : { stmts: 'N/A', branch: 'N/A', funcs: 'N/A', lines: 'N/A' }; - - // ── Read test results ── - let total = 0, passed = 0, failed = 0, skipped = 0, suites = 0, duration = '0s'; - let skippedTests = []; - try { - const files = require('child_process') - .execSync(`find "${path.join(temp, 'test-reports')}" -name test-results.json -type f`, { encoding: 'utf8' }) - .trim().split('\n').filter(Boolean); - if (files.length) { - const r = JSON.parse(fs.readFileSync(files[0], 'utf8')); - total = r.numTotalTests || 0; - passed = r.numPassedTests || 0; - failed = r.numFailedTests || 0; - skipped = r.numPendingTests || 0; - suites = r.numTotalTestSuites || 0; - const durS = Math.floor((Math.max(...r.testResults.map(t => t.endTime)) - r.startTime) / 1000); - duration = durS >= 60 ? `${Math.floor(durS / 60)}m ${durS % 60}s` : `${durS}s`; - // Collect skipped test names - for (const suite of r.testResults) { - for (const t of (suite.assertionResults || [])) { - if (t.status === 'pending' || t.status === 'skipped') { - skippedTests.push(`- ${t.ancestorTitles.join(' > ')} > ${t.title}`); - } - } - } - } - } catch {} - - // ── Coverage delta ── - function delta(pct, basePct) { - if (pct === 'N/A' || basePct === 'N/A') return '—'; - const d = (pct - basePct).toFixed(1); - const dNum = parseFloat(d); - if (dNum > 0) return `📈 +${d}%`; - if (dNum < 0) return `📉 ${d}%`; - return '='; - } - - // ── Build markdown ── - const { PR_NUMBER, QUALITY, TESTS, UBUNTU, WINDOWS, MACOS, RUN_ID, HEAD_SHA } = process.env; - const prNumber = parseInt(PR_NUMBER, 10); - const overall = (QUALITY === 'success' && TESTS === 'success') - ? '✅ **All checks passed**' : '❌ **Some checks failed**'; - const sha = HEAD_SHA.slice(0, 7); - - let body = `## CI Report\n\n${overall}   \`${sha}\`\n\n`; - - body += `### Pipeline\n\n`; - body += `| Stage | Status | Ubuntu | Windows | macOS |\n`; - body += `|-------|--------|--------|---------|-------|\n`; - body += `| Typecheck | ${icon(QUALITY)} \`${QUALITY}\` | — | — | — |\n`; - body += `| Tests | ${icon(TESTS)} \`${TESTS}\` | ${icon(UBUNTU)} | ${icon(WINDOWS)} | ${icon(MACOS)} |\n\n`; - - if (total > 0) { - body += `### Tests\n\n`; - body += `| Metric | Value |\n|--------|-------|\n`; - body += `| Total | **${total}** |\n`; - body += `| Passed | **${passed}** |\n`; - if (failed > 0) body += `| Failed | **${failed}** |\n`; - if (skipped > 0) body += `| Skipped | ${skipped} |\n`; - body += `| Files | ${suites} |\n`; - body += `| Duration | ${duration} |\n\n`; - - if (failed === 0) { - body += `✅ All **${passed}** tests passed across **${suites}** files\n`; - } else { - body += `❌ **${failed}** failed / **${passed}** passed\n`; - } + RUN_URL: ${{ github.event.workflow_run.html_url }} + run: | + DIR="$RUNNER_TEMP/artifacts" + + # ── Helper: read coverage summary into prefixed vars ── + read_cov() { + local prefix=$1 file=$2 + if [ -n "$file" ] && [ -f "$file" ]; then + local val + val=$(jq -r '.total.statements.pct // "N/A"' "$file" 2>/dev/null) || val="N/A" + printf -v "${prefix}_STMTS" '%s' "$val" + val=$(jq -r '.total.branches.pct // "N/A"' "$file" 2>/dev/null) || val="N/A" + printf -v "${prefix}_BRANCH" '%s' "$val" + val=$(jq -r '.total.functions.pct // "N/A"' "$file" 2>/dev/null) || val="N/A" + printf -v "${prefix}_FUNCS" '%s' "$val" + val=$(jq -r '.total.lines.pct // "N/A"' "$file" 2>/dev/null) || val="N/A" + printf -v "${prefix}_LINES" '%s' "$val" + val=$(jq -r '"\(.total.statements.covered)/\(.total.statements.total)"' "$file" 2>/dev/null) || val="" + printf -v "${prefix}_STMTS_COV" '%s' "$val" + val=$(jq -r '"\(.total.branches.covered)/\(.total.branches.total)"' "$file" 2>/dev/null) || val="" + printf -v "${prefix}_BRANCH_COV" '%s' "$val" + val=$(jq -r '"\(.total.functions.covered)/\(.total.functions.total)"' "$file" 2>/dev/null) || val="" + printf -v "${prefix}_FUNCS_COV" '%s' "$val" + val=$(jq -r '"\(.total.lines.covered)/\(.total.lines.total)"' "$file" 2>/dev/null) || val="" + printf -v "${prefix}_LINES_COV" '%s' "$val" + return 0 + else + printf -v "${prefix}_STMTS" '%s' "N/A" + printf -v "${prefix}_BRANCH" '%s' "N/A" + printf -v "${prefix}_FUNCS" '%s' "N/A" + printf -v "${prefix}_LINES" '%s' "N/A" + printf -v "${prefix}_STMTS_COV" '%s' "" + printf -v "${prefix}_BRANCH_COV" '%s' "" + printf -v "${prefix}_FUNCS_COV" '%s' "" + printf -v "${prefix}_LINES_COV" '%s' "" + return 1 + fi + } + + # ── Read coverage reports ── + UNIT_SUMMARY=$(find "$DIR/test-reports" -name "coverage-summary.json" -type f 2>/dev/null | head -1) + + read_cov "U" "$UNIT_SUMMARY" + + # ── Read base branch coverage (main) ── + BASE_SUMMARY="" + if [ "$BASE_FOUND" = "true" ] && [ -n "$BASE_DIR" ]; then + BASE_SUMMARY=$(find "$BASE_DIR/base" -name "coverage-summary.json" -type f 2>/dev/null | head -1) + fi + read_cov "B" "$BASE_SUMMARY" + + # ── Locate test results ── + RESULTS_FILE=$(find "$DIR/test-reports" -name "test-results.json" -type f 2>/dev/null | head -1) + + if [ -n "$RESULTS_FILE" ]; then + TOTAL=$(jq -r '.numTotalTests' "$RESULTS_FILE" 2>/dev/null || echo 0) + PASSED=$(jq -r '.numPassedTests' "$RESULTS_FILE" 2>/dev/null || echo 0) + FAILED=$(jq -r '.numFailedTests' "$RESULTS_FILE" 2>/dev/null || echo 0) + SKIPPED=$(jq -r '.numPendingTests' "$RESULTS_FILE" 2>/dev/null || echo 0) + SUITES=$(jq -r '.numTotalTestSuites' "$RESULTS_FILE" 2>/dev/null || echo 0) + DURATION=$(jq -r '((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor' "$RESULTS_FILE" 2>/dev/null || echo 0) + else + TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0; SUITES=0; DURATION=0 + fi - if (skippedTests.length > 0) { - body += `\n
\n${skipped} test(s) skipped\n\n`; - body += skippedTests.join('\n') + '\n\n
\n'; - } - body += '\n'; - } + # ── Status helpers ── + status_icon() { + case "$1" in + success) echo "✅" ;; + failure) echo "❌" ;; + cancelled) echo "⏭️" ;; + *) echo "❓" ;; + esac + } + + cov_delta() { + local pct=$1 base=$2 + if [ "$pct" = "N/A" ] || [ "$base" = "N/A" ]; then echo "—"; return; fi + local diff + diff=$(awk "BEGIN { printf \"%.1f\", $pct - $base }") + if [ "$(awk "BEGIN { print ($pct > $base) ? 1 : 0 }")" = "1" ]; then + echo "📈 +${diff}" + elif [ "$(awk "BEGIN { print ($pct < $base) ? 1 : 0 }")" = "1" ]; then + echo "📉 ${diff}" + else + echo "= ${diff}" + fi + } + + cov_bar() { + local pct=$1 base=$2 + if [ "$pct" = "N/A" ]; then echo "—"; return; fi + local filled + filled=$(awk "BEGIN { printf \"%d\", $pct / 5 }") + (( filled < 0 )) && filled=0 + (( filled > 20 )) && filled=20 + local empty=$((20 - filled)) + local bar="" + for ((i=0; i= base (or base unavailable), red if dropped + if [ "$base" = "N/A" ] || [ "$(awk "BEGIN { print ($pct >= $base) ? 1 : 0 }")" = "1" ]; then + echo "🟢 ${bar}" + else + echo "🔴 ${bar}" + fi + } + + # ── Overall status ── + if [[ "$QUALITY" == "success" && "$TESTS" == "success" && ("$E2E" == "success" || "$E2E" == "skipped") ]]; then + OVERALL="✅ **All checks passed**" + else + OVERALL="❌ **Some checks failed**" + fi - if (cov.stmts !== 'N/A') { - body += `### Coverage\n\n`; - body += `| Metric | Coverage | Covered | Base (main) | Delta |\n`; - body += `|--------|----------|---------|-------------|-------|\n`; - body += `| Statements | **${cov.stmts}%** | ${cov.stmtsCov} | ${base.stmts}% | ${delta(cov.stmts, base.stmts)} |\n`; - body += `| Branches | **${cov.branch}%** | ${cov.branchCov} | ${base.branch}% | ${delta(cov.branch, base.branch)} |\n`; - body += `| Functions | **${cov.funcs}%** | ${cov.funcsCov} | ${base.funcs}% | ${delta(cov.funcs, base.funcs)} |\n`; - body += `| Lines | **${cov.lines}%** | ${cov.linesCov} | ${base.lines}% | ${delta(cov.lines, base.lines)} |\n\n`; - } else { - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${RUN_ID}`; - body += `### Coverage\n\n⚠️ Coverage data unavailable — check the [test job](${runUrl}) for details.\n\n`; + # ── Build markdown ── + { + echo "body</dev/null; then + echo "### Test Results" + echo "" + echo "| Tests | Passed | Failed | Skipped | Duration |" + echo "|-------|--------|--------|---------|----------|" + echo "| ${TOTAL} | ${PASSED} | ${FAILED} | ${SKIPPED} | ${DURATION}s |" + echo "" + + if [ "$FAILED" = "0" ]; then + echo "✅ All **${PASSED}** tests passed" + else + echo "❌ **${FAILED}** failed / **${PASSED}** passed" + fi + if [ "$SKIPPED" != "0" ]; then + echo "" + echo "
" + echo "${SKIPPED} test(s) skipped — expand for details" + echo "" + if [ -n "$RESULTS_FILE" ]; then + jq -r ' + .testResults[] + | .assertionResults[]? + | select(.status == "pending" or .status == "skipped") + | "- \(.ancestorTitles | join(" > ")) > \(.title)" + ' "$RESULTS_FILE" 2>/dev/null || echo "- _(unable to parse skipped test details)_" + fi + echo "" + echo "
" + fi + echo "" + fi + + # ── Coverage table helper ── + cov_table() { + local label=$1 s=$2 b=$3 f=$4 l=$5 sc=$6 bc=$7 fc=$8 lc=$9 + shift 9 + local bs=$1 bb=$2 bf=$3 bl=$4 + echo "#### ${label}" + echo "" + echo "| Metric | Coverage | Covered | Base | Delta | Status |" + echo "|--------|----------|---------|------|-------|--------|" + echo "| Statements | **${s}%** | ${sc} | ${bs}% | $(cov_delta "$s" "$bs") | $(cov_bar "$s" "$bs") |" + echo "| Branches | **${b}%** | ${bc} | ${bb}% | $(cov_delta "$b" "$bb") | $(cov_bar "$b" "$bb") |" + echo "| Functions | **${f}%** | ${fc} | ${bf}% | $(cov_delta "$f" "$bf") | $(cov_bar "$f" "$bf") |" + echo "| Lines | **${l}%** | ${lc} | ${bl}% | $(cov_delta "$l" "$bl") | $(cov_bar "$l" "$bl") |" + echo "" } - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${RUN_ID}`; - body += `---\n📋 [Full run](${runUrl}) · Coverage from Ubuntu · Generated by CI`; - - // ── Post sticky comment ── - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - per_page: 100, - direction: 'desc', - }); - - const marker = ''; - const existing = comments.find(c => c.body?.includes(marker)); - const fullBody = marker + '\n' + body; - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: fullBody, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body: fullBody, - }); - } + if [ "$U_STMTS" != "N/A" ]; then + echo "### Code Coverage" + echo "" + cov_table "Tests" \ + "$U_STMTS" "$U_BRANCH" "$U_FUNCS" "$U_LINES" \ + "$U_STMTS_COV" "$U_BRANCH_COV" "$U_FUNCS_COV" "$U_LINES_COV" \ + "$B_STMTS" "$B_BRANCH" "$B_FUNCS" "$B_LINES" + else + echo "### Code Coverage" + echo "" + echo "⚠️ Coverage data unavailable - check the [unit test job](${RUN_URL}) for details." + echo "" + fi + + echo "---" + echo "📋 [View full run](${RUN_URL}) · Generated by CI" + echo "GITNEXUS_CI_REPORT_EOF_7f3a" + } >> "$GITHUB_OUTPUT" + + - name: Comment on PR + if: steps.meta.outputs.skip != 'true' + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2 + with: + header: ci-report + number: ${{ steps.meta.outputs.pr_number }} + message: ${{ steps.report.outputs.body }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ce04463c8..0e921bb748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,10 @@ concurrency: # ── Reusable workflow orchestration ───────────────────────────────── # Each concern lives in its own workflow file for maintainability: # ci-quality.yml — typecheck (tsc --noEmit) -# ci-tests.yml — all tests with coverage (ubuntu) + cross-platform -# ci-report.yml — PR comment (workflow_run trigger for fork write access) +# ci-tests.yml — unit + integration tests with coverage + cross-platform +# ci-e2e.yml — E2E tests (only when gitnexus-web/ changes) +# +# Shared setup is DRY via .github/actions/setup-gitnexus composite action. jobs: quality: @@ -30,50 +32,36 @@ jobs: permissions: contents: read - # ── Unified CI gate ────────────────────────────────────────────── - # Single required check for branch protection. - ci-status: - name: CI Gate - needs: [quality, tests] - if: always() - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Check all jobs passed - shell: bash - env: - QUALITY: ${{ needs.quality.result }} - TESTS: ${{ needs.tests.result }} - run: | - echo "Quality: $QUALITY" - echo "Tests: $TESTS" - if [[ "$QUALITY" != "success" ]] || - [[ "$TESTS" != "success" ]]; then - echo "::error::One or more CI jobs failed" - exit 1 - fi + e2e: + uses: ./.github/workflows/ci-e2e.yml + permissions: + contents: read - # ── PR metadata for ci-report.yml ──────────────────────────────── - # Saves PR number and job results so the workflow_run-triggered - # report can post comments with a write token (works for forks). + # ── Save PR metadata for the reporting workflow ───────────────── + # The ci-report.yml workflow (triggered by workflow_run) needs the + # PR number and job results to post a comment. We save them as an + # artifact because workflow_run context doesn't reliably carry PR + # info for fork PRs. save-pr-meta: name: Save PR Metadata if: always() && github.event_name == 'pull_request' - needs: [quality, tests] + needs: [quality, tests, e2e] runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Write PR metadata + - name: Write metadata shell: bash env: - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ github.event.number }} QUALITY: ${{ needs.quality.result }} TESTS: ${{ needs.tests.result }} + E2E: ${{ needs.e2e.result }} run: | mkdir -p pr-meta - echo "$PR_NUMBER" > pr-meta/pr-number - echo "$QUALITY" > pr-meta/quality-result - echo "$TESTS" > pr-meta/tests-result + echo "$PR_NUMBER" > pr-meta/pr_number + echo "$QUALITY" > pr-meta/quality_result + echo "$TESTS" > pr-meta/tests_result + echo "$E2E" > pr-meta/e2e_result - name: Upload PR metadata uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -81,3 +69,32 @@ jobs: name: pr-meta path: pr-meta/ retention-days: 1 + + # ── Unified CI gate ────────────────────────────────────────────── + # Single required check for branch protection. + ci-status: + name: CI Gate + needs: [quality, tests, e2e] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check all jobs passed + shell: bash + env: + QUALITY: ${{ needs.quality.result }} + TESTS: ${{ needs.tests.result }} + E2E: ${{ needs.e2e.result }} + run: | + echo "Quality: $QUALITY" + echo "Tests: $TESTS" + echo "E2E: $E2E" + if [[ "$QUALITY" != "success" ]] || + [[ "$TESTS" != "success" ]]; then + echo "::error::Quality or test jobs failed" + exit 1 + fi + if [[ "$E2E" != "success" && "$E2E" != "skipped" ]]; then + echo "::error::E2E job failed" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 1d3abf0746..211af5310b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ coverage/ # Misc *.local +HANDOFF.md +HANDOFF*.md .vercel @@ -57,6 +59,14 @@ assets/ # Generated files (should not be indexed) repomix-output* +# Playwright artifacts +gitnexus-web/playwright-report/ +gitnexus-web/test-results/ + +# Python test artifacts +eval/.coverage +eval/.hypothesis/ + # Design docs (local only) docs/plans/ diff --git a/gitnexus-web/e2e/debug-issues.spec.ts b/gitnexus-web/e2e/debug-issues.spec.ts new file mode 100644 index 0000000000..6ae838661f --- /dev/null +++ b/gitnexus-web/e2e/debug-issues.spec.ts @@ -0,0 +1,126 @@ +import { test, expect, type TestInfo } from '@playwright/test'; + +/** + * Debug harnesses for investigating specific UI issues. + * Excluded from `npm run test:e2e` via testIgnore in playwright.config.ts. + * Run directly: DEBUG_E2E=1 npx playwright test e2e/debug-issues.spec.ts + */ +const debugTest = process.env.DEBUG_E2E ? test : test.skip; + +async function connectToServer(page: import('@playwright/test').Page) { + page.on('console', msg => { + if (msg.type() === 'error') console.log(`[error] ${msg.text()}`); + }); + + await page.goto('/'); + await page.getByText('Server').click(); + const serverInput = page.locator('input[name="server-url-input"]'); + await serverInput.fill('http://localhost:4747'); + await page.getByRole('button', { name: /Connect/ }).click(); + await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 }); + // Wait for LadybugDB to finish loading — poll isDatabaseReady via process list visibility + await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 }); +} + +debugTest('debug: process view Reset View button', async ({ page }, testInfo) => { + await connectToServer(page); + + // Open Processes tab + await page.getByRole('button', { name: 'Nexus AI' }).click(); + await page.getByText('Processes').click(); + await expect(page.getByText(/\d+ processes detected/)).toBeVisible({ timeout: 10_000 }); + + // Click View on the first Cross-Community process + // The View button has opacity-0 by default, use JS click to bypass + const viewButtons = page.locator('button:has-text("View")'); + const count = await viewButtons.count(); + console.log(`Found ${count} View buttons`); + + // Use evaluate to click the first one regardless of visibility + await page.evaluate(() => { + const btns = document.querySelectorAll('button'); + for (const btn of btns) { + if (btn.textContent?.trim() === 'View') { + btn.click(); + return; + } + } + }); + + // Wait for modal to appear + const modal = page.locator('.fixed.inset-0.z-50'); + await expect(modal).toBeVisible({ timeout: 5_000 }); + + // Screenshot: modal should be open with flowchart + await page.screenshot({ path: testInfo.outputPath('debug-modal-open.png'), fullPage: true }); + + // Get the diagram's current transform + const diagramDiv = modal.locator('[style*="transform"]'); + const transformBefore = await diagramDiv.getAttribute('style'); + console.log('Transform BEFORE zoom:', transformBefore); + + // Zoom in using the + button + const zoomInBtn = modal.getByRole('button', { name: /Zoom in/ }); + await zoomInBtn.click(); + await zoomInBtn.click(); + await zoomInBtn.click(); + // Wait for zoom animation to settle + await expect(async () => { + const t = await diagramDiv.getAttribute('style'); + expect(t).not.toBe(transformBefore); + }).toPass({ timeout: 2_000 }); + + const transformAfterZoom = await diagramDiv.getAttribute('style'); + console.log('Transform AFTER zoom:', transformAfterZoom); + await page.screenshot({ path: testInfo.outputPath('debug-modal-zoomed.png'), fullPage: true }); + + // Click Reset View + const resetBtn = modal.getByRole('button', { name: 'Reset View' }); + await resetBtn.click(); + // Wait for reset animation to settle + await expect(async () => { + const t = await diagramDiv.getAttribute('style'); + expect(t).toBe(transformBefore); + }).toPass({ timeout: 2_000 }); + + const transformAfterReset = await diagramDiv.getAttribute('style'); + console.log('Transform AFTER reset:', transformAfterReset); + await page.screenshot({ path: testInfo.outputPath('debug-modal-after-reset.png'), fullPage: true }); + + // Verify transform actually changed back + expect(transformAfterZoom).not.toBe(transformBefore); + expect(transformAfterReset).toBe(transformBefore); +}); + +debugTest('debug: lightbulb clears node selection dimming', async ({ page }, testInfo) => { + await connectToServer(page); + + // Wait for graph canvas to render + await expect(page.locator('canvas').first()).toBeVisible({ timeout: 10_000 }); + + await page.screenshot({ path: testInfo.outputPath('debug-before-select.png'), fullPage: true }); + + // Click a file in the tree to select a node (causes dimming) + const fileItem = page.getByText('start.sh'); + await fileItem.click(); + await page.waitForLoadState('networkidle'); + await page.screenshot({ path: testInfo.outputPath('debug-node-selected.png'), fullPage: true }); + + // Check the lightbulb button state + const lightbulbBtn = page.locator('button[title*="Turn off"], button[title*="Turn on"]'); + const title = await lightbulbBtn.getAttribute('title'); + console.log('Lightbulb title before click:', title); + + // Click the lightbulb + await lightbulbBtn.click(); + await page.waitForLoadState('networkidle'); + + const titleAfter = await lightbulbBtn.getAttribute('title'); + console.log('Lightbulb title after click:', titleAfter); + await page.screenshot({ path: testInfo.outputPath('debug-after-lightbulb.png'), fullPage: true }); + + // Click it again to toggle back on + await lightbulbBtn.click(); + await page.waitForLoadState('networkidle'); + await page.screenshot({ path: testInfo.outputPath('debug-after-lightbulb-toggle-back.png'), fullPage: true }); +}); diff --git a/gitnexus-web/e2e/manual-record.spec.ts b/gitnexus-web/e2e/manual-record.spec.ts new file mode 100644 index 0000000000..794ad56946 --- /dev/null +++ b/gitnexus-web/e2e/manual-record.spec.ts @@ -0,0 +1,28 @@ +import { test } from '@playwright/test'; + +/** + * Manual recording session for interactive debugging. + * Opens the app and pauses so you can interact with the UI. + * Trace, video, and screenshots are saved automatically on close. + * + * Run with: npx playwright test e2e/manual-record.spec.ts --headed --timeout=0 + * + * Excluded from `npm run test:e2e` via testIgnore in playwright.config.ts. + * Also skipped when PWDEBUG is not set or in CI, as a safety net. + */ +test.skip( + !!process.env.CI || process.env.PWDEBUG !== '1', + 'Manual recording requires --headed and PWDEBUG=1. Run: PWDEBUG=1 npx playwright test e2e/manual-record.spec.ts --headed --timeout=0' +); + +test('manual recording session', async ({ page }) => { + page.on('console', msg => { + if (msg.type() === 'error' || msg.type() === 'warning') { + console.log(`[${msg.type()}] ${msg.text()}`); + } + }); + page.on('pageerror', err => console.log(`[crash] ${err.message}`)); + + await page.goto('http://localhost:5173'); + await page.pause(); +}); diff --git a/gitnexus-web/e2e/server-connect.spec.ts b/gitnexus-web/e2e/server-connect.spec.ts new file mode 100644 index 0000000000..f661e4abd2 --- /dev/null +++ b/gitnexus-web/e2e/server-connect.spec.ts @@ -0,0 +1,170 @@ +import { test, expect, type TestInfo } from '@playwright/test'; + +/** + * E2E tests for the GitNexus web UI. + * Requires: + * - gitnexus serve running on localhost:4747 + * - gitnexus-web dev server running on localhost:5173 + * + * Skipped when servers aren't available (CI without services, etc.). + * Set E2E=1 to force-run even without the availability check. + */ + +const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747'; +const FRONTEND_URL = process.env.FRONTEND_URL ?? 'http://localhost:5173'; +const IS_PLAYWRIGHT_AUTOMATION = process.env.PLAYWRIGHT_TEST === '1'; + +// Skip all tests if the gitnexus server or Vite dev server isn't reachable +test.beforeAll(async () => { + if (process.env.E2E) return; // force-run + try { + const [backendRes, frontendRes] = await Promise.allSettled([ + fetch(`${BACKEND_URL}/api/repos`), + fetch(FRONTEND_URL), + ]); + if (backendRes.status === 'rejected' || (backendRes.status === 'fulfilled' && !backendRes.value.ok)) + test.skip(true, 'gitnexus serve not available on :4747'); + if (frontendRes.status === 'rejected') + test.skip(true, 'Vite dev server not available on :5173'); + } catch { + test.skip(true, 'servers not available'); + } +}); + +/** Shared helper: connect to the local server and wait for the graph to load */ +async function connectAndWaitForGraph(page: import('@playwright/test').Page, testInfo: TestInfo) { + // Signal to the app that we are running under Playwright (used to skip heavy Ladybug loads). + await page.addInitScript(() => { + (window as unknown as { __PLAYWRIGHT_TEST__?: boolean }).__PLAYWRIGHT_TEST__ = true; + }); + + await page.goto('/'); + await page.screenshot({ path: testInfo.outputPath('step-1-landing.png') }); + + // Click "Server" tab in onboarding + await page.getByRole('button', { name: 'Server' }).click(); + await expect(page.getByText('Connect to Server')).toBeVisible({ timeout: 5_000 }); + await page.screenshot({ path: testInfo.outputPath('step-2-server-tab.png') }); + + // Enter server URL and connect + const serverInput = page.locator('input[name="server-url-input"]'); + await expect(serverInput).toBeVisible({ timeout: 5_000 }); + await serverInput.fill(BACKEND_URL); + await page.screenshot({ path: testInfo.outputPath('step-3-url-filled.png') }); + + await page.getByRole('button', { name: /Connect/ }).click(); + + // Wait for graph to load — status bar shows "Ready" + await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/\d+ nodes/).first()).toBeVisible(); + await page.screenshot({ path: testInfo.outputPath('step-4-graph-loaded.png') }); +} + +test.describe('Server Connection & Graph Loading', () => { + test('connects to server and loads graph', async ({ page }, testInfo) => { + await connectAndWaitForGraph(page, testInfo); + await page.screenshot({ path: testInfo.outputPath('graph-loaded.png'), fullPage: true }); + }); +}); + +test.describe('Nexus AI', () => { + test('panel opens and agent initializes without error', async ({ page }, testInfo) => { + await connectAndWaitForGraph(page, testInfo); + + // Click Nexus AI button to open the panel + await page.getByRole('button', { name: 'Nexus AI' }).click(); + + // Should see the Nexus AI tab content + await expect(page.getByText('Ask me anything')).toBeVisible({ timeout: 15_000 }); + + await page.screenshot({ path: testInfo.outputPath('nexus-ai-panel.png'), fullPage: true }); + + // "Database not ready" should NOT be visible + const errorBanner = page.getByText('Database not ready'); + expect(await errorBanner.isVisible().catch(() => false)).toBe(false); + }); +}); + +test.describe('Processes Panel', () => { + test('shows process list and View button works', async ({ page }, testInfo) => { + await connectAndWaitForGraph(page, testInfo); + + // Open Nexus AI panel, switch to Processes tab + await page.getByRole('button', { name: 'Nexus AI' }).click(); + await page.getByText('Processes').click(); + + // Should show process count — wait for data-testid instead of fixed timeout + await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ timeout: 15_000 }); + await page.screenshot({ path: testInfo.outputPath('processes-panel.png'), fullPage: true }); + + // Hover first process item to reveal View button, then click it + const processRow = page.locator('[data-testid="process-row"]').first(); + await expect(processRow).toBeVisible({ timeout: 10_000 }); + await processRow.hover(); + + const viewBtn = processRow.locator('[data-testid="process-view-button"]'); + await viewBtn.waitFor({ state: 'visible', timeout: 5_000 }); + await viewBtn.click({ force: true }); + // Wait for modal to appear + await expect(page.locator('.fixed.inset-0.z-50')).toBeVisible({ timeout: 5_000 }); + await page.screenshot({ path: testInfo.outputPath('process-view-clicked.png'), fullPage: true }); + }); + + test('lightbulb highlights nodes in graph', async ({ page }, testInfo) => { + await connectAndWaitForGraph(page, testInfo); + + await page.getByRole('button', { name: 'Nexus AI' }).click(); + await page.getByText('Processes').click(); + await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ timeout: 15_000 }); + + await page.screenshot({ path: testInfo.outputPath('before-highlight.png'), fullPage: true }); + + // Hover first process to reveal lightbulb + const processRow = page.locator('[data-testid="process-row"]').first(); + await expect(processRow).toBeVisible({ timeout: 10_000 }); + await processRow.hover(); + + const lightbulb = processRow.locator('[data-testid="process-highlight-button"]'); + await lightbulb.waitFor({ state: 'visible', timeout: 5_000 }); + await lightbulb.click({ force: true }); + // Wait for highlight animation to apply + await expect(async () => { + await expect(page.locator('canvas').first()).toBeVisible(); + }).toPass({ timeout: 2000 }); + await page.screenshot({ path: testInfo.outputPath('after-highlight.png'), fullPage: true }); + }); +}); + +test.describe('Turn Off All Highlights', () => { + test('selecting a node dims others, button clears it', async ({ page }, testInfo) => { + await connectAndWaitForGraph(page, testInfo); + + // Wait for graph to fully render by checking for canvas element + await expect(page.locator('canvas').first()).toBeVisible({ timeout: 10_000 }); + + await page.screenshot({ path: testInfo.outputPath('before-select.png'), fullPage: true }); + + // Click a file in the file tree to select a node + const fileItem = page.getByText('package.json').first(); + if (await fileItem.isVisible()) { + await fileItem.click(); + // Wait for node selection to take effect + await expect(async () => { + await expect(page.locator('canvas').first()).toBeVisible(); + }).toPass({ timeout: 2000 }); + await page.screenshot({ path: testInfo.outputPath('node-selected.png'), fullPage: true }); + + // Click "Turn off all highlights" button (top-right lightbulb) + const highlightBtn = page.locator('button[title*="Turn off"]'); + await expect(highlightBtn).toBeVisible({ timeout: 5_000 }); + await highlightBtn.click(); + // Wait for highlights to clear + await expect(async () => { + await expect(page.locator('canvas').first()).toBeVisible(); + }).toPass({ timeout: 2000 }); + await page.screenshot({ path: testInfo.outputPath('highlights-cleared.png'), fullPage: true }); + } else { + await page.screenshot({ path: testInfo.outputPath('start-sh-not-found.png'), fullPage: true }); + } + }); +}); diff --git a/gitnexus-web/package.json b/gitnexus-web/package.json index 6de001d942..aeb77824e0 100644 --- a/gitnexus-web/package.json +++ b/gitnexus-web/package.json @@ -2,16 +2,25 @@ "name": "gitnexus", "private": true, "version": "0.0.0", + "engines": { + "node": ">=20.0.0" + }, "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "test": "vitest run" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report" }, "dependencies": { "@huggingface/transformers": "^3.0.0", "@isomorphic-git/lightning-fs": "^4.6.2", + "@ladybugdb/wasm-core": "^0.15.1", "@langchain/anthropic": "^1.3.10", "@langchain/core": "^1.1.15", "@langchain/google-genai": "^2.1.10", @@ -24,22 +33,22 @@ "buffer": "^6.0.3", "comlink": "^4.4.2", "d3": "^7.9.0", + "dompurify": "^3.3.3", "graphology": "^0.26.0", "graphology-indices": "^0.17.0", - "graphology-utils": "^2.3.0", - "mnemonist": "^0.39.0", - "pandemonium": "^2.4.0", "graphology-layout-force": "^0.2.4", "graphology-layout-forceatlas2": "^0.10.1", "graphology-layout-noverlap": "^0.4.2", + "graphology-utils": "^2.3.0", "isomorphic-git": "^1.36.1", "jszip": "^3.10.1", - "@ladybugdb/wasm-core": "^0.15.2", "langchain": "^1.2.10", "lru-cache": "^11.2.4", "lucide-react": "^0.562.0", "mermaid": "^11.12.2", "minisearch": "^7.2.0", + "mnemonist": "^0.39.0", + "pandemonium": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", @@ -56,6 +65,11 @@ }, "devDependencies": { "@babel/types": "^7.28.5", + "@playwright/test": "^1.58.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/dompurify": "^3.0.5", "@types/jszip": "^3.4.0", "@types/node": "^24.10.1", "@types/react": "^18.3.5", @@ -63,10 +77,13 @@ "@types/react-syntax-highlighter": "^15.5.13", "@vercel/node": "^5.5.16", "@vitejs/plugin-react": "^5.1.0", + "@vitest/coverage-v8": "^3.2.4", + "jsdom": "^29.0.0", "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", "vite": "^5.2.0", "vite-plugin-static-copy": "^3.1.4", - "vitest": "^4.0.18" + "vitest": "^3.2.4", + "wait-on": "^8.0.5" } } diff --git a/gitnexus-web/playwright.config.ts b/gitnexus-web/playwright.config.ts new file mode 100644 index 0000000000..f98a515f08 --- /dev/null +++ b/gitnexus-web/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from '@playwright/test'; + +// Enable insecure browser config (disabled security + CSP bypass) only when explicitly requested. +// Example: PLAYWRIGHT_INSECURE=1 npx playwright test +const insecureE2E = process.env.PLAYWRIGHT_INSECURE === '1'; + +// Base launch args: always enable software WebGL for sigma.js graph rendering in headless mode. +const launchArgs = [ + '--use-gl=angle', + '--use-angle=swiftshader', + '--enable-webgl', + '--enable-unsafe-swiftshader', +]; + +if (insecureE2E) { + // Allow cross-origin requests to gitnexus serve on a different port when explicitly enabled. + launchArgs.unshift('--disable-web-security', '--disable-site-isolation-trials'); +} + +export default defineConfig({ + testDir: './e2e', + testIgnore: ['**/manual-record.spec.ts', '**/debug-issues.spec.ts'], + timeout: 60_000, + retries: 0, + use: { + baseURL: 'http://localhost:5173', + trace: 'retain-on-failure', + screenshot: 'retain-on-failure', + video: 'retain-on-failure', + launchOptions: { + args: launchArgs, + }, + // Vite dev server sets COEP require-corp for SharedArrayBuffer (LadybugDB WASM). + // Only bypass CSP when explicitly running in insecure E2E mode. + bypassCSP: insecureE2E, + }, + projects: [ + { + name: 'chromium', + use: { browserName: 'chromium' }, + }, + ], + reporter: [ + ['list'], + ['html', { open: 'never', outputFolder: 'playwright-report' }], + ], + outputDir: 'test-results', +}); diff --git a/gitnexus-web/test/fixtures/graph.ts b/gitnexus-web/test/fixtures/graph.ts new file mode 100644 index 0000000000..a8814e80ee --- /dev/null +++ b/gitnexus-web/test/fixtures/graph.ts @@ -0,0 +1,66 @@ +/** + * Shared test data factories for graph structures. + * No test code — pure data exports. + */ + +import type { GraphNode, GraphRelationship } from '../../src/core/graph/types'; + +export function createFileNode(name: string, filePath?: string): GraphNode { + return { + id: `File:${filePath ?? name}`, + label: 'File', + properties: { name, filePath: filePath ?? name }, + }; +} + +export function createFunctionNode(name: string, filePath: string, line = 1): GraphNode { + return { + id: `Function:${filePath}:${name}:${line}`, + label: 'Function', + properties: { name, filePath, startLine: line, endLine: line + 10 }, + }; +} + +export function createClassNode(name: string, filePath: string): GraphNode { + return { + id: `Class:${filePath}:${name}`, + label: 'Class', + properties: { name, filePath }, + }; +} + +export function createProcessNode(id: string, label: string, type: 'cross_community' | 'intra_community' = 'cross_community'): GraphNode { + return { + id, + label: 'Process', + properties: { + name: label, + heuristicLabel: label, + processType: type, + stepCount: 3, + communities: ['cluster-a', 'cluster-b'], + } as any, + }; +} + +export function createCallsRelationship(sourceId: string, targetId: string): GraphRelationship { + return { + id: `${sourceId}_CALLS_${targetId}`, + sourceId, + targetId, + type: 'CALLS', + confidence: 0.9, + reason: 'same-file', + }; +} + +export function createContainsRelationship(sourceId: string, targetId: string): GraphRelationship { + return { + id: `${sourceId}_CONTAINS_${targetId}`, + sourceId, + targetId, + type: 'CONTAINS', + confidence: 1.0, + reason: '', + }; +} diff --git a/gitnexus-web/test/setup.ts b/gitnexus-web/test/setup.ts index c6a2e2d532..860ee70c15 100644 --- a/gitnexus-web/test/setup.ts +++ b/gitnexus-web/test/setup.ts @@ -1,6 +1,8 @@ import { beforeEach } from 'vitest'; +import '@testing-library/jest-dom/vitest'; +// Reset storage between tests beforeEach(() => { - sessionStorage.clear(); - localStorage.clear(); + sessionStorage.removeItem('gitnexus-llm-settings'); + localStorage.removeItem('gitnexus-llm-settings'); // legacy key (migration) }); diff --git a/gitnexus-web/test/unit/constants.test.ts b/gitnexus-web/test/unit/constants.test.ts new file mode 100644 index 0000000000..c886dfdfd1 --- /dev/null +++ b/gitnexus-web/test/unit/constants.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { + NODE_COLORS, + NODE_SIZES, + COMMUNITY_COLORS, + getCommunityColor, + DEFAULT_VISIBLE_LABELS, + FILTERABLE_LABELS, + ALL_EDGE_TYPES, + DEFAULT_VISIBLE_EDGES, + EDGE_INFO, +} from '../../src/lib/constants'; + +describe('NODE_COLORS', () => { + it('has a color for every node label used in NODE_SIZES', () => { + for (const label of Object.keys(NODE_SIZES)) { + expect(NODE_COLORS).toHaveProperty(label); + expect(NODE_COLORS[label as keyof typeof NODE_COLORS]).toMatch(/^#[0-9a-f]{6}$/i); + } + }); +}); + +describe('NODE_SIZES', () => { + it('gives Project the largest size', () => { + const maxLabel = Object.entries(NODE_SIZES).reduce((a, b) => a[1] > b[1] ? a : b); + expect(maxLabel[0]).toBe('Project'); + }); + + it('gives structural nodes larger sizes than code nodes', () => { + expect(NODE_SIZES.Folder).toBeGreaterThan(NODE_SIZES.Function); + expect(NODE_SIZES.File).toBeGreaterThan(NODE_SIZES.Variable); + }); +}); + +describe('getCommunityColor', () => { + it('returns valid hex colors', () => { + for (let i = 0; i < 20; i++) { + expect(getCommunityColor(i)).toMatch(/^#[0-9a-f]{6}$/i); + } + }); + + it('wraps around the palette', () => { + const paletteSize = COMMUNITY_COLORS.length; + expect(getCommunityColor(0)).toBe(getCommunityColor(paletteSize)); + expect(getCommunityColor(1)).toBe(getCommunityColor(paletteSize + 1)); + }); +}); + +describe('DEFAULT_VISIBLE_LABELS', () => { + it('includes common structural and code labels', () => { + expect(DEFAULT_VISIBLE_LABELS).toContain('File'); + expect(DEFAULT_VISIBLE_LABELS).toContain('Function'); + expect(DEFAULT_VISIBLE_LABELS).toContain('Class'); + }); + + it('excludes noisy labels by default', () => { + expect(DEFAULT_VISIBLE_LABELS).not.toContain('Variable'); + expect(DEFAULT_VISIBLE_LABELS).not.toContain('Import'); + }); +}); + +describe('edge types', () => { + it('ALL_EDGE_TYPES contains all EDGE_INFO keys', () => { + const edgeInfoKeys = Object.keys(EDGE_INFO).sort(); + const allEdgeTypes = [...ALL_EDGE_TYPES].sort(); + expect(edgeInfoKeys).toEqual(allEdgeTypes); + }); + + it('DEFAULT_VISIBLE_EDGES is a subset of ALL_EDGE_TYPES', () => { + for (const type of DEFAULT_VISIBLE_EDGES) { + expect(ALL_EDGE_TYPES).toContain(type); + } + }); + + it('EDGE_INFO entries have color and label', () => { + for (const info of Object.values(EDGE_INFO)) { + expect(info.color).toMatch(/^#[0-9a-f]{6}$/i); + expect(info.label.length).toBeGreaterThan(0); + } + }); +}); diff --git a/gitnexus-web/test/unit/graph.test.ts b/gitnexus-web/test/unit/graph.test.ts new file mode 100644 index 0000000000..8c62e4454c --- /dev/null +++ b/gitnexus-web/test/unit/graph.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { createKnowledgeGraph } from '../../src/core/graph/graph'; +import { createFileNode, createFunctionNode, createCallsRelationship, createContainsRelationship } from '../fixtures/graph'; + +describe('createKnowledgeGraph', () => { + it('starts empty', () => { + const graph = createKnowledgeGraph(); + expect(graph.nodeCount).toBe(0); + expect(graph.relationshipCount).toBe(0); + expect(graph.nodes).toEqual([]); + expect(graph.relationships).toEqual([]); + }); + + it('adds nodes', () => { + const graph = createKnowledgeGraph(); + const node = createFileNode('index.ts', 'src/index.ts'); + graph.addNode(node); + + expect(graph.nodeCount).toBe(1); + expect(graph.nodes[0].id).toBe('File:src/index.ts'); + }); + + it('deduplicates nodes by id', () => { + const graph = createKnowledgeGraph(); + const node = createFileNode('index.ts', 'src/index.ts'); + const duplicateNode = createFileNode('index.ts', 'src/index.ts'); + graph.addNode(node); + graph.addNode(duplicateNode); + + expect(graph.nodeCount).toBe(1); + }); + + it('adds relationships', () => { + const graph = createKnowledgeGraph(); + const rel = createCallsRelationship('fn:a', 'fn:b'); + graph.addRelationship(rel); + + expect(graph.relationshipCount).toBe(1); + expect(graph.relationships[0].type).toBe('CALLS'); + }); + + it('deduplicates relationships by id', () => { + const graph = createKnowledgeGraph(); + const rel = createCallsRelationship('fn:a', 'fn:b'); + const duplicateRel = createCallsRelationship('fn:a', 'fn:b'); + graph.addRelationship(rel); + graph.addRelationship(duplicateRel); + + expect(graph.relationshipCount).toBe(1); + }); + + it('builds a multi-node graph', () => { + const graph = createKnowledgeGraph(); + const file = createFileNode('app.ts', 'src/app.ts'); + const fn1 = createFunctionNode('main', 'src/app.ts', 1); + const fn2 = createFunctionNode('helper', 'src/app.ts', 20); + + graph.addNode(file); + graph.addNode(fn1); + graph.addNode(fn2); + graph.addRelationship(createContainsRelationship(file.id, fn1.id)); + graph.addRelationship(createContainsRelationship(file.id, fn2.id)); + graph.addRelationship(createCallsRelationship(fn1.id, fn2.id)); + + expect(graph.nodeCount).toBe(3); + expect(graph.relationshipCount).toBe(3); + }); +}); diff --git a/gitnexus-web/test/unit/mermaid-generator.test.ts b/gitnexus-web/test/unit/mermaid-generator.test.ts new file mode 100644 index 0000000000..6f0b64a6be --- /dev/null +++ b/gitnexus-web/test/unit/mermaid-generator.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest'; +import { generateProcessMermaid, generateSimpleMermaid } from '../../src/lib/mermaid-generator'; +import type { ProcessData } from '../../src/lib/mermaid-generator'; + +describe('generateProcessMermaid', () => { + it('returns placeholder for empty steps', () => { + const process: ProcessData = { + id: 'p1', + label: 'Empty', + processType: 'intra_community', + steps: [], + }; + expect(generateProcessMermaid(process)).toContain('No steps found'); + }); + + it('generates a linear chain without edges', () => { + const process: ProcessData = { + id: 'p1', + label: 'GET -> Handler', + processType: 'intra_community', + steps: [ + { id: 'fn:a', name: 'handleGet', filePath: 'src/routes.ts', stepNumber: 1 }, + { id: 'fn:b', name: 'validate', filePath: 'src/validate.ts', stepNumber: 2 }, + { id: 'fn:c', name: 'respond', filePath: 'src/respond.ts', stepNumber: 3 }, + ], + }; + + const result = generateProcessMermaid(process); + expect(result).toContain('graph TD'); + expect(result).toContain('handleGet'); + expect(result).toContain('validate'); + expect(result).toContain('respond'); + // Linear chain: a -> b -> c + expect(result).toContain('-->'); + }); + + it('uses CALLS edges when provided', () => { + const process: ProcessData = { + id: 'p1', + label: 'Branching', + processType: 'intra_community', + steps: [ + { id: 'fn:a', name: 'entry', filePath: 'src/a.ts', stepNumber: 1 }, + { id: 'fn:b', name: 'branchA', filePath: 'src/b.ts', stepNumber: 2 }, + { id: 'fn:c', name: 'branchB', filePath: 'src/c.ts', stepNumber: 3 }, + ], + edges: [ + { from: 'fn:a', to: 'fn:b', type: 'CALLS' }, + { from: 'fn:a', to: 'fn:c', type: 'CALLS' }, + ], + }; + + const result = generateProcessMermaid(process); + // Both edges should appear + expect(result).toContain('fn_a --> fn_b'); + expect(result).toContain('fn_a --> fn_c'); + }); + + it('applies entry and terminal classes', () => { + const process: ProcessData = { + id: 'p1', + label: 'Flow', + processType: 'intra_community', + steps: [ + { id: 'fn:start', name: 'start', filePath: 'src/a.ts', stepNumber: 1 }, + { id: 'fn:end', name: 'end', filePath: 'src/b.ts', stepNumber: 2 }, + ], + }; + + const result = generateProcessMermaid(process); + expect(result).toContain(':::entry'); + expect(result).toContain(':::terminal'); + }); + + it('uses subgraphs for cross-community processes with clusters', () => { + const process: ProcessData = { + id: 'p1', + label: 'Cross', + processType: 'cross_community', + steps: [ + { id: 'fn:a', name: 'a', filePath: 'src/a.ts', stepNumber: 1, cluster: 'Auth' }, + { id: 'fn:b', name: 'b', filePath: 'src/b.ts', stepNumber: 2, cluster: 'DB' }, + ], + }; + + const result = generateProcessMermaid(process); + expect(result).toContain('subgraph'); + expect(result).toContain('Auth'); + expect(result).toContain('DB'); + }); +}); + +describe('generateSimpleMermaid', () => { + it('generates a preview with entry and terminal', () => { + const result = generateSimpleMermaid('POST -> ShouldRedact', 5); + expect(result).toContain('graph LR'); + expect(result).toContain('POST'); + expect(result).toContain('ShouldRedact'); + expect(result).toContain('3 steps'); + }); + + it('handles labels without arrow', () => { + const result = generateSimpleMermaid('SingleNode', 2); + expect(result).toContain('graph LR'); + expect(result).toContain('SingleNode'); + }); +}); diff --git a/gitnexus-web/test/unit/path-resolution.test.ts b/gitnexus-web/test/unit/path-resolution.test.ts new file mode 100644 index 0000000000..e3bd59221a --- /dev/null +++ b/gitnexus-web/test/unit/path-resolution.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { normalizePath, resolveFilePath } from '../../src/lib/path-resolution'; + +describe('path-resolution utilities', () => { + const contents = new Map([ + ['src/components/Header.tsx', ''], + ['src/core/utils/index.ts', ''], + ['README.md', ''], + ['src/lib/path-resolution.ts', ''], + ]); + + it('normalizes leading ./ and backslashes', () => { + expect(normalizePath('./src\\components\\Header.tsx')).toBe('src/components/Header.tsx'); + }); + + it('prefers exact matches', () => { + expect(resolveFilePath(contents, 'src/components/Header.tsx')).toBe('src/components/Header.tsx'); + }); + + it('resolves ends-with partials', () => { + expect(resolveFilePath(contents, 'core/utils/index.ts')).toBe('src/core/utils/index.ts'); + }); + + it('falls back to segment matching', () => { + expect(resolveFilePath(contents, 'lib/path')).toBe('src/lib/path-resolution.ts'); + }); + + it('returns null for empty requests', () => { + expect(resolveFilePath(contents, '')).toBeNull(); + }); +}); diff --git a/gitnexus-web/test/unit/server-connection.test.ts b/gitnexus-web/test/unit/server-connection.test.ts new file mode 100644 index 0000000000..28228f0c0a --- /dev/null +++ b/gitnexus-web/test/unit/server-connection.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeServerUrl, extractFileContents } from '../../src/services/server-connection'; +import type { GraphNode } from '../../src/core/graph/types'; + +describe('normalizeServerUrl', () => { + it('adds http:// to localhost', () => { + expect(normalizeServerUrl('localhost:4747')).toBe('http://localhost:4747/api'); + }); + + it('adds http:// to 127.0.0.1', () => { + expect(normalizeServerUrl('127.0.0.1:4747')).toBe('http://127.0.0.1:4747/api'); + }); + + it('adds https:// to non-local hosts', () => { + expect(normalizeServerUrl('example.com')).toBe('https://example.com/api'); + }); + + it('strips trailing slashes', () => { + expect(normalizeServerUrl('http://localhost:4747/')).toBe('http://localhost:4747/api'); + expect(normalizeServerUrl('http://localhost:4747///')).toBe('http://localhost:4747/api'); + }); + + it('does not double-append /api', () => { + expect(normalizeServerUrl('http://localhost:4747/api')).toBe('http://localhost:4747/api'); + }); + + it('trims whitespace', () => { + expect(normalizeServerUrl(' localhost:4747 ')).toBe('http://localhost:4747/api'); + }); + + it('preserves existing https://', () => { + expect(normalizeServerUrl('https://gitnexus.example.com')).toBe('https://gitnexus.example.com/api'); + }); +}); + +describe('extractFileContents', () => { + it('extracts content from File nodes', () => { + const nodes: GraphNode[] = [ + { + id: 'File:src/index.ts', + label: 'File', + properties: { name: 'index.ts', filePath: 'src/index.ts', content: 'console.log("hello")' } as any, + }, + ]; + const result = extractFileContents(nodes); + expect(result['src/index.ts']).toBe('console.log("hello")'); + }); + + it('ignores non-File nodes', () => { + const nodes: GraphNode[] = [ + { + id: 'Function:main', + label: 'Function', + properties: { name: 'main', filePath: 'src/index.ts', content: 'fn body' } as any, + }, + ]; + const result = extractFileContents(nodes); + expect(Object.keys(result)).toHaveLength(0); + }); + + it('ignores File nodes without content', () => { + const nodes: GraphNode[] = [ + { + id: 'File:src/empty.ts', + label: 'File', + properties: { name: 'empty.ts', filePath: 'src/empty.ts' }, + }, + ]; + const result = extractFileContents(nodes); + expect(Object.keys(result)).toHaveLength(0); + }); + + it('returns empty object for empty input', () => { + expect(extractFileContents([])).toEqual({}); + }); +}); diff --git a/gitnexus-web/test/unit/settings-service.test.ts b/gitnexus-web/test/unit/settings-service.test.ts new file mode 100644 index 0000000000..19065b5129 --- /dev/null +++ b/gitnexus-web/test/unit/settings-service.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest'; +import { + loadSettings, + saveSettings, + setActiveProvider, + getActiveProviderConfig, + isProviderConfigured, + clearSettings, + getProviderDisplayName, + getAvailableModels, +} from '../../src/core/llm/settings-service'; + +describe('loadSettings', () => { + it('returns defaults when nothing is stored', () => { + const settings = loadSettings(); + expect(settings.activeProvider).toBeDefined(); + expect(settings.openai).toBeDefined(); + expect(settings.ollama).toBeDefined(); + }); + + it('merges stored values with defaults', () => { + sessionStorage.setItem('gitnexus-llm-settings', JSON.stringify({ + activeProvider: 'ollama', + ollama: { model: 'qwen3-coder:30b' }, + })); + + const settings = loadSettings(); + expect(settings.activeProvider).toBe('ollama'); + expect(settings.ollama.model).toBe('qwen3-coder:30b'); + // Should still have other provider defaults + expect(settings.openai).toBeDefined(); + }); + + it('returns defaults on corrupted JSON', () => { + sessionStorage.setItem('gitnexus-llm-settings', 'not-json{{{'); + const settings = loadSettings(); + expect(settings.activeProvider).toBeDefined(); + }); + + it('migrates legacy localStorage to sessionStorage', () => { + localStorage.setItem('gitnexus-llm-settings', JSON.stringify({ + activeProvider: 'ollama', + ollama: { model: 'migrated-model' }, + })); + + const settings = loadSettings(); + expect(settings.ollama.model).toBe('migrated-model'); + expect(sessionStorage.getItem('gitnexus-llm-settings')).not.toBeNull(); + expect(localStorage.getItem('gitnexus-llm-settings')).toBeNull(); + }); +}); + +describe('saveSettings / clearSettings', () => { + it('persists settings to sessionStorage', () => { + const settings = loadSettings(); + settings.activeProvider = 'anthropic'; + saveSettings(settings); + expect(loadSettings().activeProvider).toBe('anthropic'); + }); + + it('clearSettings removes settings from both storages', () => { + saveSettings({ ...loadSettings(), activeProvider: 'anthropic' }); + expect(sessionStorage.getItem('gitnexus-llm-settings')).not.toBeNull(); + clearSettings(); + expect(sessionStorage.getItem('gitnexus-llm-settings')).toBeNull(); + expect(localStorage.getItem('gitnexus-llm-settings')).toBeNull(); + }); +}); + +describe('setActiveProvider', () => { + it('changes the active provider and persists', () => { + setActiveProvider('gemini'); + expect(loadSettings().activeProvider).toBe('gemini'); + }); +}); + +describe('getActiveProviderConfig', () => { + it('returns null for unconfigured providers requiring API keys', () => { + setActiveProvider('openai'); + expect(getActiveProviderConfig()).toBeNull(); + }); + + it('returns config for ollama without API key', () => { + setActiveProvider('ollama'); + const config = getActiveProviderConfig(); + expect(config).not.toBeNull(); + expect(config!.provider).toBe('ollama'); + }); + + it('returns config for openai when API key is set', () => { + const settings = loadSettings(); + settings.activeProvider = 'openai'; + settings.openai = { ...settings.openai, apiKey: 'sk-test-123' }; + saveSettings(settings); + + const config = getActiveProviderConfig(); + expect(config).not.toBeNull(); + expect(config!.provider).toBe('openai'); + }); + + it('returns null for openrouter with empty API key', () => { + const settings = loadSettings(); + settings.activeProvider = 'openrouter'; + settings.openrouter = { ...settings.openrouter, apiKey: ' ' }; + saveSettings(settings); + + expect(getActiveProviderConfig()).toBeNull(); + }); +}); + +describe('isProviderConfigured', () => { + it('returns false when provider requires API key and none is set', () => { + // Manually build a clean openai config with no API key + saveSettings({ ...loadSettings(), activeProvider: 'openai', openai: { apiKey: '', model: 'gpt-4o', temperature: 0.1 } }); + expect(isProviderConfigured()).toBe(false); + }); + + it('returns true for ollama (no key required)', () => { + setActiveProvider('ollama'); + expect(isProviderConfigured()).toBe(true); + }); +}); + +describe('getProviderDisplayName', () => { + it('returns human-readable names', () => { + expect(getProviderDisplayName('openai')).toBe('OpenAI'); + expect(getProviderDisplayName('azure-openai')).toBe('Azure OpenAI'); + expect(getProviderDisplayName('gemini')).toBe('Google Gemini'); + expect(getProviderDisplayName('anthropic')).toBe('Anthropic'); + expect(getProviderDisplayName('ollama')).toBe('Ollama (Local)'); + expect(getProviderDisplayName('openrouter')).toBe('OpenRouter'); + }); +}); + +describe('getAvailableModels', () => { + it('returns models for known providers', () => { + expect(getAvailableModels('openai').length).toBeGreaterThan(0); + expect(getAvailableModels('ollama').length).toBeGreaterThan(0); + expect(getAvailableModels('anthropic')).toContain('claude-sonnet-4-20250514'); + }); + + it('returns empty array for unknown provider', () => { + expect(getAvailableModels('unknown' as any)).toEqual([]); + }); +}); diff --git a/gitnexus-web/test/unit/utils.test.ts b/gitnexus-web/test/unit/utils.test.ts new file mode 100644 index 0000000000..066b06a6e1 --- /dev/null +++ b/gitnexus-web/test/unit/utils.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { generateId } from '../../src/lib/utils'; + +describe('generateId', () => { + it('creates label:name format', () => { + expect(generateId('File', 'index.ts')).toBe('File:index.ts'); + expect(generateId('Function', 'main')).toBe('Function:main'); + }); + + it('handles empty strings', () => { + expect(generateId('', '')).toBe(':'); + }); + + it('preserves special characters in name', () => { + expect(generateId('File', 'src/components/App.tsx')).toBe('File:src/components/App.tsx'); + }); +}); diff --git a/gitnexus-web/vitest.config.ts b/gitnexus-web/vitest.config.ts index d2d63450d2..1af686ab5b 100644 --- a/gitnexus-web/vitest.config.ts +++ b/gitnexus-web/vitest.config.ts @@ -1,17 +1,40 @@ import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ - test: { - environment: 'jsdom', - globals: true, - setupFiles: ['test/setup.ts'], - include: ['test/**/*.test.ts'], - exclude: ['**/node_modules/**', '**/dist/**'], - }, + plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), + '@anthropic-ai/sdk/lib/transform-json-schema': path.resolve(__dirname, 'node_modules/@anthropic-ai/sdk/lib/transform-json-schema.mjs'), + 'mermaid': path.resolve(__dirname, 'node_modules/mermaid/dist/mermaid.esm.min.mjs'), + }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./test/setup.ts'], + include: ['test/**/*.test.{ts,tsx}'], + testTimeout: 15000, + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/workers/**', // Web workers (require worker env) + 'src/core/lbug/**', // WASM (requires SharedArrayBuffer) + 'src/core/tree-sitter/**', // WASM (requires tree-sitter binaries) + 'src/core/embeddings/**', // WASM (requires ML model) + 'src/main.tsx', // Entry point + 'src/vite-env.d.ts', // Type declarations + ], + thresholds: { + statements: 10, + branches: 10, + functions: 10, + lines: 10, + }, }, }, }); + diff --git a/gitnexus/vitest.config.ts b/gitnexus/vitest.config.ts index 475bf8eadd..fbec3724b5 100644 --- a/gitnexus/vitest.config.ts +++ b/gitnexus/vitest.config.ts @@ -32,7 +32,6 @@ export default defineConfig({ branches: 23, functions: 28, lines: 27, - autoUpdate: true, }, }, @@ -90,3 +89,4 @@ export default defineConfig({ ], }, }); + From 7bc0e73eaaff5919985ed86217c41c6d324aee5c Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 04:08:59 -0500 Subject: [PATCH 02/13] chore: update gitnexus-web package-lock.json Reflects devDependency additions (vitest, playwright, wait-on, @testing-library, etc.) from package.json changes in this PR. Co-Authored-By: Claude Opus 4.6 (1M context) --- gitnexus-web/package-lock.json | 2780 ++++++++++++++++++++++---------- 1 file changed, 1912 insertions(+), 868 deletions(-) diff --git a/gitnexus-web/package-lock.json b/gitnexus-web/package-lock.json index 392117c8ee..b7be8fcb1e 100644 --- a/gitnexus-web/package-lock.json +++ b/gitnexus-web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@huggingface/transformers": "^3.0.0", "@isomorphic-git/lightning-fs": "^4.6.2", - "@ladybugdb/wasm-core": "^0.15.2", + "@ladybugdb/wasm-core": "^0.15.1", "@langchain/anthropic": "^1.3.10", "@langchain/core": "^1.1.15", "@langchain/google-genai": "^2.1.10", @@ -23,6 +23,7 @@ "buffer": "^6.0.3", "comlink": "^4.4.2", "d3": "^7.9.0", + "dompurify": "^3.3.3", "graphology": "^0.26.0", "graphology-indices": "^0.17.0", "graphology-layout-force": "^0.2.4", @@ -54,6 +55,11 @@ }, "devDependencies": { "@babel/types": "^7.28.5", + "@playwright/test": "^1.58.2", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/dompurify": "^3.0.5", "@types/jszip": "^3.4.0", "@types/node": "^24.10.1", "@types/react": "^18.3.5", @@ -61,11 +67,38 @@ "@types/react-syntax-highlighter": "^15.5.13", "@vercel/node": "^5.5.16", "@vitejs/plugin-react": "^5.1.0", + "@vitest/coverage-v8": "^3.2.4", + "jsdom": "^29.0.0", "tree-sitter-wasms": "^0.1.13", "typescript": "^5.4.5", "vite": "^5.2.0", "vite-plugin-static-copy": "^3.1.4", - "vitest": "^4.0.18" + "vitest": "^3.2.4", + "wait-on": "^8.0.5" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/@antfu/install-pkg": { @@ -101,6 +134,47 @@ } } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -132,7 +206,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -430,12 +503,35 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@braintree/sanitize-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", "license": "MIT" }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@cfworker/json-schema": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", @@ -517,6 +613,146 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@edge-runtime/format": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.1.tgz", @@ -571,9 +807,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "license": "MIT", "optional": true, "dependencies": { @@ -1043,6 +1279,24 @@ "node": ">=18" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -1062,6 +1316,60 @@ "node": ">=18.0.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@huggingface/jinja": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.3.tgz", @@ -1588,6 +1896,24 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1621,6 +1947,16 @@ "superblocktxt": "src/superblocktxt.js" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1667,9 +2003,9 @@ } }, "node_modules/@ladybugdb/wasm-core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@ladybugdb/wasm-core/-/wasm-core-0.15.2.tgz", - "integrity": "sha512-KIR+DBKPMEJlyBxESJF0t/hwQhtlshyhJwj7L5d8nF98R0+J/4bFYZN3mqzagQyi5CfMeIVqRp7gTxfJZ/gEuw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@ladybugdb/wasm-core/-/wasm-core-0.15.1.tgz", + "integrity": "sha512-dHEq8inJQBkHnJrqZMKGdltSfeSv9OHECkzWQixqDLApXXGlbJ5Ugq5rRfk2PLJuZ74LVHT0cZvcn4JLmsnAIA==", "license": "MIT", "dependencies": { "threads": "^1.7.0", @@ -1711,7 +2047,6 @@ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.15.tgz", "integrity": "sha512-b8RN5DkWAmDAlMu/UpTZEluYwCLpm63PPWniRKlE8ie3KkkE7IuMQ38pf4kV1iaiI+d99BEQa2vafQHfCujsRA==", "license": "MIT", - "peer": true, "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", @@ -2027,14 +2362,31 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/@protobufjs/aspromise": { @@ -2101,307 +2453,34 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", - "cpu": [ - "arm64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-virtual": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", - "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3238,6 +3317,96 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@ts-morph/common": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", @@ -3302,6 +3471,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3627,7 +3804,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/estree": { + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", @@ -3694,7 +3881,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3716,7 +3902,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3746,8 +3931,8 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", @@ -3905,61 +4090,131 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", - "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", - "magic-string": "^0.30.21", + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", "pathe": "^2.0.3" }, "funding": { @@ -3967,25 +4222,28 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4018,7 +4276,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4076,6 +4333,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -4122,6 +4389,16 @@ "dev": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4132,6 +4409,35 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-listen": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", @@ -4234,6 +4540,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4308,7 +4624,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4347,6 +4662,16 @@ "ieee754": "^1.2.1" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4447,11 +4772,18 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, "engines": { "node": ">=18" } @@ -4527,6 +4859,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chevrotain": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", @@ -4751,6 +5093,42 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4762,7 +5140,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -5163,7 +5540,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5258,6 +5634,58 @@ "lodash-es": "^4.17.21" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", @@ -5290,6 +5718,13 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -5318,6 +5753,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5423,10 +5868,18 @@ "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5446,6 +5899,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/edge-runtime": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", @@ -5494,6 +5954,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -5507,6 +5974,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5837,6 +6317,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -6247,6 +6744,26 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -6318,6 +6835,16 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6408,6 +6935,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6477,6 +7014,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -6498,8 +7042,15 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, - "node_modules/isomorphic-git": { - "version": "1.36.1", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-git": { + "version": "1.36.1", "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.36.1.tgz", "integrity": "sha512-fC8SRT8MwoaXDK8G4z5biPEbqf2WyEJUb2MJ2ftSd39/UIlsnoZxLGux+lae0poLZO4AEcx6aUVOh5bV+P8zFA==", "license": "MIT", @@ -6532,6 +7083,76 @@ "fast-text-encoding": "^1.0.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", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -6541,6 +7162,25 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tiktoken": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", @@ -6556,6 +7196,95 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", + "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", + "integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7057,6 +7786,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.22", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", @@ -7091,6 +7827,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -7106,9 +7849,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -7123,6 +7866,17 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -7132,6 +7886,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -7464,6 +8246,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8138,6 +8927,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -8363,17 +9162,6 @@ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==", "license": "MIT" }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, "node_modules/ollama": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz", @@ -8521,6 +9309,13 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/package-manager-detector": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", @@ -8577,6 +9372,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -8590,6 +9398,16 @@ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "license": "MIT" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", @@ -8628,6 +9446,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8673,6 +9501,53 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -8726,6 +9601,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/pretty-ms": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", @@ -8842,7 +9733,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8855,7 +9745,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8864,6 +9753,14 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/react-markdown": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -8977,6 +9874,20 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/refractor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", @@ -9113,53 +10024,11 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, - "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", - "dev": true, - "license": "MIT" - }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9241,6 +10110,16 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9267,6 +10146,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -9396,19 +10288,41 @@ "@img/sharp-win32-x64": "0.34.5" } }, - "node_modules/siginfo": { + "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "ISC" - }, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sigma": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.2.tgz", "integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==", "license": "MIT", - "peer": true, "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" @@ -9511,9 +10425,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -9526,6 +10440,60 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -9540,6 +10508,82 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -9576,6 +10620,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -9611,6 +10662,139 @@ "node": ">=18" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/threads": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", @@ -9687,16 +10871,56 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.25" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -9724,6 +10948,19 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9842,8 +11079,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "devOptional": true, + "license": "0BSD" }, "node_modules/type-fest": { "version": "0.13.1", @@ -9877,7 +11114,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10113,7 +11349,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -10168,6 +11403,36 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/vite-plugin-static-copy": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz", @@ -10631,71 +11896,65 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@opentelemetry/api": { + "@types/debug": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { + "@vitest/browser": { "optional": true }, "@vitest/ui": { @@ -10706,407 +11965,16 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false } } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.0", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, - "node_modules/vitest/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/vitest/node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/vitest/node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -11156,6 +12024,39 @@ "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -11175,6 +12076,16 @@ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11186,6 +12097,22 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -11224,12 +12151,130 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -11254,7 +12299,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From a40e5bded676241607409c39a61656dadf7af406 Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 13:50:13 -0500 Subject: [PATCH 03/13] fix(e2e): add missing process-list-loaded testid, increase CI timeouts - Add data-testid="process-list-loaded" to ProcessesPanel (E2E tests were waiting for an element that didn't exist) - Increase server connect timeouts from 5s to 10s for slower CI Co-Authored-By: Claude Opus 4.6 (1M context) --- gitnexus-web/e2e/server-connect.spec.ts | 4 ++-- gitnexus-web/src/components/ProcessesPanel.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gitnexus-web/e2e/server-connect.spec.ts b/gitnexus-web/e2e/server-connect.spec.ts index f661e4abd2..cb3f87e7e8 100644 --- a/gitnexus-web/e2e/server-connect.spec.ts +++ b/gitnexus-web/e2e/server-connect.spec.ts @@ -43,12 +43,12 @@ async function connectAndWaitForGraph(page: import('@playwright/test').Page, tes // Click "Server" tab in onboarding await page.getByRole('button', { name: 'Server' }).click(); - await expect(page.getByText('Connect to Server')).toBeVisible({ timeout: 5_000 }); + await expect(page.getByText('Connect to Server')).toBeVisible({ timeout: 10_000 }); await page.screenshot({ path: testInfo.outputPath('step-2-server-tab.png') }); // Enter server URL and connect const serverInput = page.locator('input[name="server-url-input"]'); - await expect(serverInput).toBeVisible({ timeout: 5_000 }); + await expect(serverInput).toBeVisible({ timeout: 10_000 }); await serverInput.fill(BACKEND_URL); await page.screenshot({ path: testInfo.outputPath('step-3-url-filled.png') }); diff --git a/gitnexus-web/src/components/ProcessesPanel.tsx b/gitnexus-web/src/components/ProcessesPanel.tsx index 39ff5f7577..086a603691 100644 --- a/gitnexus-web/src/components/ProcessesPanel.tsx +++ b/gitnexus-web/src/components/ProcessesPanel.tsx @@ -326,7 +326,7 @@ export const ProcessesPanel = () => { /> -
+
{totalCount} processes detected
From 478159b6fcd8c87fc79bbb04844d376133c0ed38 Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 13:53:51 -0500 Subject: [PATCH 04/13] fix(ci): run gitnexus-web unit tests in CI, remove unused variable - Add gitnexus-web npm ci + vitest run to ci-tests.yml so web unit tests are gated by the CI status check (were only running locally) - Remove unused IS_PLAYWRIGHT_AUTOMATION variable from E2E spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci-tests.yml | 8 ++++++++ gitnexus-web/e2e/server-connect.spec.ts | 2 -- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index c48401bcd5..453fa35397 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -28,6 +28,14 @@ jobs: --coverage.reportOnFailure=true working-directory: gitnexus + - name: Install gitnexus-web dependencies + run: npm ci + working-directory: gitnexus-web + + - name: Run gitnexus-web unit tests + run: npx vitest run --reporter=default + working-directory: gitnexus-web + - name: Upload test reports if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/gitnexus-web/e2e/server-connect.spec.ts b/gitnexus-web/e2e/server-connect.spec.ts index cb3f87e7e8..668f95fcb3 100644 --- a/gitnexus-web/e2e/server-connect.spec.ts +++ b/gitnexus-web/e2e/server-connect.spec.ts @@ -12,8 +12,6 @@ import { test, expect, type TestInfo } from '@playwright/test'; const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747'; const FRONTEND_URL = process.env.FRONTEND_URL ?? 'http://localhost:5173'; -const IS_PLAYWRIGHT_AUTOMATION = process.env.PLAYWRIGHT_TEST === '1'; - // Skip all tests if the gitnexus server or Vite dev server isn't reachable test.beforeAll(async () => { if (process.env.E2E) return; // force-run From 9fc3e7a272a50e1fdd0601e552f9927aa045097b Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 14:23:30 -0500 Subject: [PATCH 05/13] fix(e2e): add process-row testid, wait for networkidle on page load - Add data-testid="process-row" to ProcessItem component (E2E tests referenced it but it didn't exist in the source) - Use waitUntil: 'networkidle' on page.goto to ensure Vite dev server is fully ready before interacting (fixes first-test timeout in CI) Co-Authored-By: Claude Opus 4.6 (1M context) --- gitnexus-web/e2e/server-connect.spec.ts | 2 +- gitnexus-web/src/components/ProcessesPanel.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitnexus-web/e2e/server-connect.spec.ts b/gitnexus-web/e2e/server-connect.spec.ts index 668f95fcb3..fd3390ea9f 100644 --- a/gitnexus-web/e2e/server-connect.spec.ts +++ b/gitnexus-web/e2e/server-connect.spec.ts @@ -36,7 +36,7 @@ async function connectAndWaitForGraph(page: import('@playwright/test').Page, tes (window as unknown as { __PLAYWRIGHT_TEST__?: boolean }).__PLAYWRIGHT_TEST__ = true; }); - await page.goto('/'); + await page.goto('/', { waitUntil: 'networkidle' }); await page.screenshot({ path: testInfo.outputPath('step-1-landing.png') }); // Click "Server" tab in onboarding diff --git a/gitnexus-web/src/components/ProcessesPanel.tsx b/gitnexus-web/src/components/ProcessesPanel.tsx index 086a603691..f1e1e9692e 100644 --- a/gitnexus-web/src/components/ProcessesPanel.tsx +++ b/gitnexus-web/src/components/ProcessesPanel.tsx @@ -462,7 +462,7 @@ const ProcessItem = ({ process, isLoading, isSelected, isFocused, onView, onTogg : ''; return ( -
+
{process.label}
From 75ca65d421a1adcad6be7c63524b0a638b0ce4ec Mon Sep 17 00:00:00 2001 From: jreakin Date: Mon, 23 Mar 2026 14:26:09 -0500 Subject: [PATCH 06/13] fix(e2e): add process-view-button and process-highlight-button testids E2E tests referenced these data-testid attributes but they didn't exist in ProcessItem. All 6 E2E testids now have matching source elements: status-ready, process-list-loaded, process-row, process-view-button, process-highlight-button, server-url-input. Co-Authored-By: Claude Opus 4.6 (1M context) --- gitnexus-web/src/components/ProcessesPanel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gitnexus-web/src/components/ProcessesPanel.tsx b/gitnexus-web/src/components/ProcessesPanel.tsx index f1e1e9692e..094dd7d007 100644 --- a/gitnexus-web/src/components/ProcessesPanel.tsx +++ b/gitnexus-web/src/components/ProcessesPanel.tsx @@ -484,12 +484,14 @@ const ProcessItem = ({ process, isLoading, isSelected, isFocused, onView, onTogg : 'text-text-muted hover:text-cyan-400 bg-white/5 hover:bg-cyan-500/20 border border-white/10 hover:border-cyan-400/40 opacity-0 group-hover:opacity-100' }`} title={isFocused ? 'Click to remove highlight from graph' : 'Click to highlight in graph'} + data-testid="process-highlight-button" > diff --git a/gitnexus-web/src/components/ProcessFlowModal.tsx b/gitnexus-web/src/components/ProcessFlowModal.tsx index 93017f3768..e452b98569 100644 --- a/gitnexus-web/src/components/ProcessFlowModal.tsx +++ b/gitnexus-web/src/components/ProcessFlowModal.tsx @@ -211,6 +211,7 @@ export const ProcessFlowModal = ({ process, onClose, onFocusInGraph, isFullScree ref={containerRef} className="fixed inset-0 z-50 flex items-center justify-center bg-black/20 animate-fade-in" onClick={handleBackdropClick} + data-testid="process-modal" > {/* Glassmorphism Modal */}
Date: Mon, 23 Mar 2026 20:18:24 -0500 Subject: [PATCH 12/13] fix(ci): include gitnexus-web test results in CI report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web unit tests ran in CI but only output to stdout — results weren't in the JSON artifact so they were missing from the PR report. Now emits web-test-results.json, uploads it, and the report sums both CLI and web test counts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci-report.yml | 47 ++++++++++++++++++++------------- .github/workflows/ci-tests.yml | 7 ++++- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index 4d4628e69f..d49929663c 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -241,17 +241,26 @@ jobs: # ── Locate test results ── RESULTS_FILE=$(find "$DIR/test-reports" -name "test-results.json" -type f 2>/dev/null | head -1) + WEB_RESULTS_FILE=$(find "$DIR/test-reports" -name "web-test-results.json" -type f 2>/dev/null | head -1) - if [ -n "$RESULTS_FILE" ]; then - TOTAL=$(jq -r '.numTotalTests' "$RESULTS_FILE" 2>/dev/null || echo 0) - PASSED=$(jq -r '.numPassedTests' "$RESULTS_FILE" 2>/dev/null || echo 0) - FAILED=$(jq -r '.numFailedTests' "$RESULTS_FILE" 2>/dev/null || echo 0) - SKIPPED=$(jq -r '.numPendingTests' "$RESULTS_FILE" 2>/dev/null || echo 0) - SUITES=$(jq -r '.numTotalTestSuites' "$RESULTS_FILE" 2>/dev/null || echo 0) - DURATION=$(jq -r '((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor' "$RESULTS_FILE" 2>/dev/null || echo 0) - else - TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0; SUITES=0; DURATION=0 - fi + sum_results() { + local file=$1 + if [ -n "$file" ] && [ -f "$file" ]; then + jq -r '"\(.numTotalTests) \(.numPassedTests) \(.numFailedTests) \(.numPendingTests) \(.numTotalTestSuites) \(((.testResults | map(.endTime) | max) - (.startTime)) / 1000 | floor)"' "$file" 2>/dev/null || echo "0 0 0 0 0 0" + else + echo "0 0 0 0 0 0" + fi + } + + read CLI_T CLI_P CLI_F CLI_S CLI_SU CLI_D <<< "$(sum_results "$RESULTS_FILE")" + read WEB_T WEB_P WEB_F WEB_S WEB_SU WEB_D <<< "$(sum_results "$WEB_RESULTS_FILE")" + + TOTAL=$((CLI_T + WEB_T)) + PASSED=$((CLI_P + WEB_P)) + FAILED=$((CLI_F + WEB_F)) + SKIPPED=$((CLI_S + WEB_S)) + SUITES=$((CLI_SU + WEB_SU)) + DURATION=$((CLI_D > WEB_D ? CLI_D : WEB_D)) # ── Status helpers ── status_icon() { @@ -337,14 +346,16 @@ jobs: echo "
" echo "${SKIPPED} test(s) skipped — expand for details" echo "" - if [ -n "$RESULTS_FILE" ]; then - jq -r ' - .testResults[] - | .assertionResults[]? - | select(.status == "pending" or .status == "skipped") - | "- \(.ancestorTitles | join(" > ")) > \(.title)" - ' "$RESULTS_FILE" 2>/dev/null || echo "- _(unable to parse skipped test details)_" - fi + for rf in "$RESULTS_FILE" "$WEB_RESULTS_FILE"; do + if [ -n "$rf" ] && [ -f "$rf" ]; then + jq -r ' + .testResults[] + | .assertionResults[]? + | select(.status == "pending" or .status == "skipped") + | "- \(.ancestorTitles | join(" > ")) > \(.title)" + ' "$rf" 2>/dev/null || true + fi + done echo "" echo "
" fi diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 453fa35397..e100c5bff4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -33,7 +33,11 @@ jobs: working-directory: gitnexus-web - name: Run gitnexus-web unit tests - run: npx vitest run --reporter=default + run: >- + npx vitest run + --reporter=default + --reporter=json + --outputFile=web-test-results.json working-directory: gitnexus-web - name: Upload test reports @@ -45,6 +49,7 @@ jobs: gitnexus/coverage/coverage-summary.json gitnexus/coverage/coverage-final.json gitnexus/test-results.json + gitnexus-web/web-test-results.json retention-days: 5 cross-platform: From af734cbdf5dc24bdedab0fcf05a007acdeb9adf8 Mon Sep 17 00:00:00 2001 From: jreakin Date: Tue, 24 Mar 2026 00:47:29 -0500 Subject: [PATCH 13/13] =?UTF-8?q?fix(ci):=20harden=20CI=20report=20?= =?UTF-8?q?=E2=80=94=20awk=20injection,=20dir=20resilience,=20artifact=20c?= =?UTF-8?q?ompat,=20node=20alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sanitize coverage values with is_numeric() validation and awk -v flag parameterization to prevent command injection from untrusted fork artifacts - Add mkdir -p before unzip in base coverage extraction step - Write both underscore and hyphenated artifact filenames so main-branch ci-report.yml (via workflow_run) can read them before this PR merges - Align Node version: engines >=18→>=20, Dockerfile.test 22→20 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci-report.yml | 19 +++++++++++++------ .github/workflows/ci.yml | 10 ++++++++++ gitnexus/Dockerfile.test | 2 +- gitnexus/package.json | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index d49929663c..2e117f859c 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -176,6 +176,7 @@ jobs: shell: bash run: | cd "${{ steps.base-coverage.outputs.dir }}" + mkdir -p base unzip -o base.zip -d base - name: Build report @@ -272,14 +273,20 @@ jobs: esac } + # Validate a value looks like a number (integer or decimal, optional + # leading minus). Returns 1 for anything else — guards against awk + # injection when artifact values come from untrusted fork code. + is_numeric() { [[ "$1" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; } + cov_delta() { local pct=$1 base=$2 if [ "$pct" = "N/A" ] || [ "$base" = "N/A" ]; then echo "—"; return; fi + if ! is_numeric "$pct" || ! is_numeric "$base"; then echo "—"; return; fi local diff - diff=$(awk "BEGIN { printf \"%.1f\", $pct - $base }") - if [ "$(awk "BEGIN { print ($pct > $base) ? 1 : 0 }")" = "1" ]; then + diff=$(awk -v p="$pct" -v b="$base" 'BEGIN { printf "%.1f", p - b }') + if [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p > b) ? 1 : 0 }')" = "1" ]; then echo "📈 +${diff}" - elif [ "$(awk "BEGIN { print ($pct < $base) ? 1 : 0 }")" = "1" ]; then + elif [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p < b) ? 1 : 0 }')" = "1" ]; then echo "📉 ${diff}" else echo "= ${diff}" @@ -288,9 +295,9 @@ jobs: cov_bar() { local pct=$1 base=$2 - if [ "$pct" = "N/A" ]; then echo "—"; return; fi + if [ "$pct" = "N/A" ] || ! is_numeric "$pct"; then echo "—"; return; fi local filled - filled=$(awk "BEGIN { printf \"%d\", $pct / 5 }") + filled=$(awk -v p="$pct" 'BEGIN { printf "%d", p / 5 }') (( filled < 0 )) && filled=0 (( filled > 20 )) && filled=20 local empty=$((20 - filled)) @@ -298,7 +305,7 @@ jobs: for ((i=0; i= base (or base unavailable), red if dropped - if [ "$base" = "N/A" ] || [ "$(awk "BEGIN { print ($pct >= $base) ? 1 : 0 }")" = "1" ]; then + if [ "$base" = "N/A" ] || ! is_numeric "$base" || [ "$(awk -v p="$pct" -v b="$base" 'BEGIN { print (p >= b) ? 1 : 0 }')" = "1" ]; then echo "🟢 ${bar}" else echo "🔴 ${bar}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e921bb748..ba7184ace8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,16 @@ jobs: echo "$QUALITY" > pr-meta/quality_result echo "$TESTS" > pr-meta/tests_result echo "$E2E" > pr-meta/e2e_result + # TODO(post-merge): remove backward-compat copies once ci-report.yml + # on main reads underscore names. + # Backward-compat: ci-report.yml on main still reads hyphenated + # names. workflow_run always executes from the default branch, so + # the main-branch reader won't find the underscore variants until + # this PR is merged. Write both until then. + cp pr-meta/pr_number pr-meta/pr-number + cp pr-meta/quality_result pr-meta/quality-result + cp pr-meta/tests_result pr-meta/tests-result + cp pr-meta/e2e_result pr-meta/e2e-result - name: Upload PR metadata uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/gitnexus/Dockerfile.test b/gitnexus/Dockerfile.test index 9d68cf2d47..b2d22384f2 100644 --- a/gitnexus/Dockerfile.test +++ b/gitnexus/Dockerfile.test @@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:20-bookworm WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* COPY . . diff --git a/gitnexus/package.json b/gitnexus/package.json index 4182565f1f..b422666ab0 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -103,6 +103,6 @@ "tree-sitter": "0.22.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }