diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..9cdf140b --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,187 @@ +name: Build and Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +env: + TESTLINK_URL: http://localhost:8090 + TESTLINK_PROJECT_ID: "1" + OLLAMA_HOST: http://localhost:11434 + +jobs: + build: + name: Build Docker Images + runs-on: [self-hosted, k80, cuda11] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install test runner dependencies + run: cd tests && npm ci + + - name: Run build tests + id: build-tests + run: | + cd tests + npm run dev -- run --suite build --no-llm --output json > /tmp/build-results.json 2>&1 || true + cat /tmp/build-results.json + + # Check if any tests failed + if grep -q '"passed": false' /tmp/build-results.json; then + echo "Some build tests failed" + exit 1 + fi + + - name: Upload build results + uses: actions/upload-artifact@v4 + if: always() + with: + name: build-test-results + path: /tmp/build-results.json + + runtime: + name: Runtime Tests + runs-on: [self-hosted, k80, cuda11] + needs: build + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install test runner dependencies + run: cd tests && npm ci + + - name: Start container + run: | + cd docker + docker compose down 2>/dev/null || true + docker compose up -d + sleep 10 + + - name: Run runtime tests + id: runtime-tests + run: | + cd tests + npm run dev -- run --suite runtime --no-llm --output json > /tmp/runtime-results.json 2>&1 || true + cat /tmp/runtime-results.json + + - name: Upload runtime results + uses: actions/upload-artifact@v4 + if: always() + with: + name: runtime-test-results + path: /tmp/runtime-results.json + + inference: + name: Inference Tests + runs-on: [self-hosted, k80, cuda11] + needs: runtime + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install test runner dependencies + run: cd tests && npm ci + + - name: Run inference tests + id: inference-tests + run: | + cd tests + npm run dev -- run --suite inference --no-llm --output json > /tmp/inference-results.json 2>&1 || true + cat /tmp/inference-results.json + + - name: Upload inference results + uses: actions/upload-artifact@v4 + if: always() + with: + name: inference-test-results + path: /tmp/inference-results.json + + llm-judge: + name: LLM Judge Evaluation + runs-on: [self-hosted, k80, cuda11] + needs: [build, runtime, inference] + if: always() + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install test runner dependencies + run: cd tests && npm ci + + - name: Download all test results + uses: actions/download-artifact@v4 + with: + path: /tmp/results + + - name: Run LLM judge on all results + run: | + cd tests + echo "Running LLM judge evaluation..." + + # Re-run all tests with LLM judge using local Ollama + npm run dev -- run --output json > /tmp/llm-judged-results.json 2>&1 || true + cat /tmp/llm-judged-results.json + + - name: Upload final results + uses: actions/upload-artifact@v4 + if: always() + with: + name: llm-judged-results + path: /tmp/llm-judged-results.json + + cleanup: + name: Cleanup + runs-on: [self-hosted, k80, cuda11] + needs: [build, runtime, inference, llm-judge] + if: always() + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Stop Container + run: | + cd docker + docker compose down || true + echo "Container stopped" + + - name: Summary + run: | + echo "## Build and Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Runtime | ${{ needs.runtime.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Inference | ${{ needs.inference.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| LLM Judge | ${{ needs.llm-judge.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index e689b382..beaabacb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ .venv .swp dist -build +/build +!tests/testcases/build .cache .gocache *.exe @@ -16,3 +17,4 @@ llama/build llama/vendor /ollama docker/output/ +tests/node_modules/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index c09a24d9..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This file is a template, and might need editing before it works on your project. -# This is a sample GitLab CI/CD configuration file that should run without any modifications. -# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts, -# it uses echo commands to simulate the pipeline execution. -# -# A pipeline is composed of independent jobs that run scripts, grouped into stages. -# Stages run in sequential order, but jobs within stages run in parallel. -# -# For more information, see: https://docs.gitlab.com/ee/ci/yaml/#stages -# -# You can copy and paste this template into a new `.gitlab-ci.yml` file. -# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. -# -# To contribute improvements to CI/CD templates, please follow the Development guide at: -# https://docs.gitlab.com/development/cicd/templates/ -# This specific template is located at: -# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml - -stages: # List of stages for jobs, and their order of execution - - build - - test - - deploy - -build-job: # This job runs in the build stage, which runs first. - stage: build - script: - - echo "Compiling the code..." - - echo "Compile complete." - -unit-test-job: # This job runs in the test stage. - stage: test # It only starts when the job in the build stage completes successfully. - script: - - echo "Running unit tests... This will take about 60 seconds." - - sleep 60 - - echo "Code coverage is 90%" - -lint-test-job: # This job also runs in the test stage. - stage: test # It can run at the same time as unit-test-job (in parallel). - script: - - echo "Linting code... This will take about 10 seconds." - - sleep 10 - - echo "No lint issues found." - -deploy-job: # This job runs in the deploy stage. - stage: deploy # It only runs when *both* jobs in the test stage complete successfully. - environment: production - script: - - echo "Deploying application..." - - echo "Application successfully deployed." diff --git a/docs/CICD.md b/docs/CICD.md new file mode 100644 index 00000000..0c1068c7 --- /dev/null +++ b/docs/CICD.md @@ -0,0 +1,318 @@ +# CI/CD Plan for Ollama37 + +This document describes the CI/CD pipeline for building and testing Ollama37 with Tesla K80 (CUDA compute capability 3.7) support. + +## Infrastructure Overview + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ GITHUB │ +│ dogkeeper886/ollama37 │ +│ │ +│ Push to main ──────────────────────────────────────────────────────┐ │ +└─────────────────────────────────────────────────────────────────────│───┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ CI/CD NODE │ +│ │ +│ Hardware: │ +│ - Tesla K80 GPU (compute capability 3.7) │ +│ - NVIDIA Driver 470.x │ +│ │ +│ Software: │ +│ - Rocky Linux 9.7 │ +│ - Docker 29.1.3 + Docker Compose 5.0.0 │ +│ - NVIDIA Container Toolkit │ +│ - GitHub Actions Runner (self-hosted, labels: k80, cuda11) │ +│ │ +│ Services: │ +│ - TestLink (http://localhost:8090) - Test management │ +│ - TestLink MCP - Claude Code integration │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ SERVE NODE │ +│ │ +│ Services: │ +│ - Ollama (production) │ +│ - Dify (LLM application platform) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Build Strategy: Docker-Based + +We use the two-stage Docker build system located in `/docker/`: + +### Stage 1: Builder Image (Cached) + +**Image:** `ollama37-builder:latest` (~15GB) + +**Contents:** +- Rocky Linux 8 +- CUDA 11.4 toolkit +- GCC 10 (built from source) +- CMake 4.0 (built from source) +- Go 1.25.3 + +**Build time:** ~90 minutes (first time only, then cached) + +**Build command:** +```bash +cd docker && make build-builder +``` + +### Stage 2: Runtime Image (Per Build) + +**Image:** `ollama37:latest` (~18GB) + +**Process:** +1. Clone source from GitHub +2. Configure with CMake ("CUDA 11" preset) +3. Build C/C++/CUDA libraries +4. Build Go binary +5. Package runtime environment + +**Build time:** ~10 minutes + +**Build command:** +```bash +cd docker && make build-runtime +``` + +## Pipeline Stages + +### Stage 1: Docker Build + +**Trigger:** Push to `main` branch + +**Steps:** +1. Checkout repository +2. Ensure builder image exists (build if not) +3. Build runtime image: `make build-runtime` +4. Verify image created successfully + +**Test Cases:** +- TC-BUILD-001: Builder Image Verification +- TC-BUILD-002: Runtime Image Build +- TC-BUILD-003: Image Size Validation + +### Stage 2: Container Startup + +**Steps:** +1. Start container with GPU: `docker compose up -d` +2. Wait for health check to pass +3. Verify Ollama server is responding + +**Test Cases:** +- TC-RUNTIME-001: Container Startup +- TC-RUNTIME-002: GPU Detection +- TC-RUNTIME-003: Health Check + +### Stage 3: Inference Tests + +**Steps:** +1. Pull test model (gemma3:4b) +2. Run inference tests +3. Verify CUBLAS legacy fallback + +**Test Cases:** +- TC-INFERENCE-001: Model Pull +- TC-INFERENCE-002: Basic Inference +- TC-INFERENCE-003: API Endpoint Test +- TC-INFERENCE-004: CUBLAS Fallback Verification + +### Stage 4: Cleanup & Report + +**Steps:** +1. Stop container: `docker compose down` +2. Report results to TestLink +3. Clean up resources + +## Test Case Design + +### Build Tests (Suite: Build Tests) + +| ID | Name | Type | Description | +|----|------|------|-------------| +| TC-BUILD-001 | Builder Image Verification | Automated | Verify builder image exists with correct tools | +| TC-BUILD-002 | Runtime Image Build | Automated | Build runtime image from GitHub source | +| TC-BUILD-003 | Image Size Validation | Automated | Verify image sizes are within expected range | + +### Runtime Tests (Suite: Runtime Tests) + +| ID | Name | Type | Description | +|----|------|------|-------------| +| TC-RUNTIME-001 | Container Startup | Automated | Start container with GPU passthrough | +| TC-RUNTIME-002 | GPU Detection | Automated | Verify Tesla K80 detected inside container | +| TC-RUNTIME-003 | Health Check | Automated | Verify Ollama health check passes | + +### Inference Tests (Suite: Inference Tests) + +| ID | Name | Type | Description | +|----|------|------|-------------| +| TC-INFERENCE-001 | Model Pull | Automated | Pull gemma3:4b model | +| TC-INFERENCE-002 | Basic Inference | Automated | Run simple prompt and verify response | +| TC-INFERENCE-003 | API Endpoint Test | Automated | Test /api/generate endpoint | +| TC-INFERENCE-004 | CUBLAS Fallback Verification | Automated | Verify legacy CUBLAS functions used | + +## GitHub Actions Workflow + +**File:** `.github/workflows/build-test.yml` + +**Triggers:** +- Push to `main` branch +- Pull request to `main` branch +- Manual trigger (workflow_dispatch) + +**Runner:** Self-hosted with labels `[self-hosted, k80, cuda11]` + +**Jobs:** +1. `build` - Build Docker runtime image +2. `test` - Run inference tests in container +3. `report` - Report results to TestLink + +## TestLink Integration + +**URL:** http://localhost:8090 + +**Project:** ollama37 + +**Test Suites:** +- Build Tests +- Runtime Tests +- Inference Tests + +**Test Plan:** Created per release/sprint + +**Builds:** Created per CI run (commit SHA) + +**Execution Recording:** +- Each test case result recorded via TestLink API +- Pass/Fail status with notes +- Linked to specific build/commit + +## Makefile Targets for CI + +| Target | Description | When to Use | +|--------|-------------|-------------| +| `make build-builder` | Build base image | First time setup | +| `make build-runtime` | Build from GitHub | Normal CI builds | +| `make build-runtime-no-cache` | Fresh GitHub clone | When cache is stale | +| `make build-runtime-local` | Build from local | Local testing | + +## Environment Variables + +### Build Environment + +| Variable | Value | Description | +|----------|-------|-------------| +| `BUILDER_IMAGE` | ollama37-builder | Builder image name | +| `RUNTIME_IMAGE` | ollama37 | Runtime image name | + +### Runtime Environment + +| Variable | Value | Description | +|----------|-------|-------------| +| `OLLAMA_HOST` | 0.0.0.0:11434 | Server listen address | +| `NVIDIA_VISIBLE_DEVICES` | all | GPU visibility | +| `OLLAMA_DEBUG` | 1 (optional) | Enable debug logging | +| `GGML_CUDA_DEBUG` | 1 (optional) | Enable CUDA debug | + +### TestLink Environment + +| Variable | Value | Description | +|----------|-------|-------------| +| `TESTLINK_URL` | http://localhost:8090 | TestLink server URL | +| `TESTLINK_API_KEY` | (configured) | API key for automation | + +## Prerequisites + +### One-Time Setup on CI/CD Node + +1. **Install GitHub Actions Runner:** + ```bash + mkdir -p ~/actions-runner && cd ~/actions-runner + curl -o actions-runner-linux-x64-2.321.0.tar.gz -L \ + https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz + tar xzf ./actions-runner-linux-x64-2.321.0.tar.gz + ./config.sh --url https://github.com/dogkeeper886/ollama37 --token YOUR_TOKEN --labels k80,cuda11 + sudo ./svc.sh install && sudo ./svc.sh start + ``` + +2. **Build Builder Image (one-time, ~90 min):** + ```bash + cd /home/jack/src/ollama37/docker + make build-builder + ``` + +3. **Verify GPU Access in Docker:** + ```bash + docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi + ``` + +4. **Start TestLink:** + ```bash + cd /home/jack/src/testlink-code + docker compose up -d + ``` + +## Monitoring & Logs + +### View CI/CD Logs + +```bash +# GitHub Actions Runner logs +journalctl -u actions.runner.* -f + +# Docker build logs +docker compose logs -f + +# TestLink logs +cd /home/jack/src/testlink-code && docker compose logs -f +``` + +### Test Results + +- **TestLink Dashboard:** http://localhost:8090 +- **GitHub Actions:** https://github.com/dogkeeper886/ollama37/actions + +## Troubleshooting + +### Builder Image Missing + +```bash +cd docker && make build-builder +``` + +### GPU Not Detected in Container + +```bash +# Check UVM device files on host +ls -l /dev/nvidia-uvm* + +# Create if missing +nvidia-modprobe -u -c=0 + +# Restart container +docker compose restart +``` + +### Build Cache Stale + +```bash +cd docker && make build-runtime-no-cache +``` + +### TestLink Connection Failed + +```bash +# Check TestLink is running +curl http://localhost:8090 + +# Restart if needed +cd /home/jack/src/testlink-code && docker compose restart +``` diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 00000000..73c96539 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,1426 @@ +{ + "name": "ollama37-test-runner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ollama37-test-runner", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "glob": "^10.3.10", + "js-yaml": "^4.1.0", + "p-limit": "^5.0.0" + }, + "bin": { + "ollama37-test": "dist/cli.js" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.14.0", + "tsx": "^4.16.0", + "typescript": "^5.5.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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==", + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "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", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "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/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "BlueOak-1.0.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "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==", + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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==", + "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==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 00000000..84708d00 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,33 @@ +{ + "name": "ollama37-test-runner", + "version": "1.0.0", + "description": "Scalable test runner with LLM-as-judge for ollama37", + "type": "module", + "main": "dist/index.js", + "bin": { + "ollama37-test": "dist/cli.js" + }, + "scripts": { + "build": "tsc", + "start": "node dist/cli.js", + "dev": "tsx src/cli.ts", + "test": "tsx src/cli.ts run", + "test:build": "tsx src/cli.ts run --suite build", + "test:runtime": "tsx src/cli.ts run --suite runtime", + "test:inference": "tsx src/cli.ts run --suite inference" + }, + "dependencies": { + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "glob": "^10.3.10", + "js-yaml": "^4.1.0", + "p-limit": "^5.0.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.14.0", + "tsx": "^4.16.0", + "typescript": "^5.5.0" + } +} diff --git a/tests/src/cli.ts b/tests/src/cli.ts new file mode 100644 index 00000000..85463545 --- /dev/null +++ b/tests/src/cli.ts @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +import { Command } from 'commander' +import { writeFileSync } from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { TestLoader } from './loader.js' +import { TestExecutor } from './executor.js' +import { LLMJudge } from './judge.js' +import { Reporter, TestLinkReporter } from './reporter.js' +import { RunnerOptions } from './types.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const defaultTestcasesDir = path.join(__dirname, '..', 'testcases') + +const program = new Command() + +program + .name('ollama37-test') + .description('Scalable test runner with LLM-as-judge for ollama37') + .version('1.0.0') + +program + .command('run') + .description('Run test cases') + .option('-s, --suite ', 'Run only tests in specified suite (build, runtime, inference)') + .option('-i, --id ', 'Run only specified test case by ID') + .option('-w, --workers ', 'Number of parallel workers', '1') + .option('-d, --dry-run', 'Show what would be executed without running') + .option('-o, --output ', 'Output format: console, json, junit', 'console') + .option('--report-testlink', 'Report results to TestLink') + .option('--ollama-url ', 'Ollama server URL', 'http://localhost:11434') + .option('--ollama-model ', 'Ollama model for judging', 'gemma3:4b') + .option('--testlink-url ', 'TestLink server URL', 'http://localhost:8090') + .option('--testlink-api-key ', 'TestLink API key') + .option('--no-llm', 'Skip LLM judging, use simple exit code check') + .option('--testcases-dir ', 'Test cases directory', defaultTestcasesDir) + .action(async (options) => { + console.log('='.repeat(60)) + console.log('OLLAMA37 TEST RUNNER') + console.log('='.repeat(60)) + + const loader = new TestLoader(options.testcasesDir) + const executor = new TestExecutor(path.join(__dirname, '..', '..')) + const judge = new LLMJudge(options.ollamaUrl, options.ollamaModel) + + // Load test cases + console.log('\nLoading test cases...') + let testCases = await loader.loadAll() + + if (options.suite) { + testCases = testCases.filter(tc => tc.suite === options.suite) + console.log(` Filtered by suite: ${options.suite}`) + } + + if (options.id) { + testCases = testCases.filter(tc => tc.id === options.id) + console.log(` Filtered by ID: ${options.id}`) + } + + // Sort by dependencies + testCases = loader.sortByDependencies(testCases) + + console.log(` Found ${testCases.length} test cases`) + + if (testCases.length === 0) { + console.log('\nNo test cases found!') + process.exit(1) + } + + // Dry run + if (options.dryRun) { + console.log('\nDRY RUN - Would execute:') + for (const tc of testCases) { + console.log(` ${tc.id}: ${tc.name}`) + for (const step of tc.steps) { + console.log(` - ${step.name}: ${step.command}`) + } + } + process.exit(0) + } + + // Execute tests + console.log('\nExecuting tests...') + const workers = parseInt(options.workers) + const results = await executor.executeAll(testCases, workers) + + // Judge results + console.log('\nJudging results...') + let judgments + if (options.llm === false) { + console.log(' Using simple exit code check (--no-llm)') + judgments = results.map(r => judge.simpleJudge(r)) + } else { + try { + judgments = await judge.judgeResults(results) + } catch (error) { + console.error(' LLM judging failed, falling back to simple check:', error) + judgments = results.map(r => judge.simpleJudge(r)) + } + } + + // Create reports + const reports = Reporter.createReports(results, judgments) + + // Output results + switch (options.output) { + case 'json': + const json = Reporter.toJSON(reports) + console.log(json) + writeFileSync('test-results.json', json) + console.log('\nResults written to test-results.json') + break + + case 'junit': + const junit = Reporter.toJUnit(reports) + writeFileSync('test-results.xml', junit) + console.log('\nResults written to test-results.xml') + break + + case 'console': + default: + Reporter.toConsole(reports) + break + } + + // Report to TestLink + if (options.reportTestlink && options.testlinkApiKey) { + const testlinkReporter = new TestLinkReporter( + options.testlinkUrl, + options.testlinkApiKey + ) + // Would need plan ID and build ID + // await testlinkReporter.reportResults(reports, planId, buildId) + console.log('\nTestLink reporting not yet implemented') + } + + // Exit with appropriate code + const failed = reports.filter(r => !r.pass).length + process.exit(failed > 0 ? 1 : 0) + }) + +program + .command('list') + .description('List all test cases') + .option('--testcases-dir ', 'Test cases directory', defaultTestcasesDir) + .action(async (options) => { + const loader = new TestLoader(options.testcasesDir) + const testCases = await loader.loadAll() + + const grouped = loader.groupBySuite(testCases) + + console.log('Available Test Cases:\n') + for (const [suite, cases] of grouped) { + console.log(`${suite.toUpperCase()}:`) + for (const tc of cases) { + console.log(` ${tc.id}: ${tc.name}`) + } + console.log() + } + + console.log(`Total: ${testCases.length} test cases`) + }) + +program.parse() diff --git a/tests/src/executor.ts b/tests/src/executor.ts new file mode 100644 index 00000000..fbaa46af --- /dev/null +++ b/tests/src/executor.ts @@ -0,0 +1,119 @@ +import { exec } from 'child_process' +import { promisify } from 'util' +import { TestCase, TestResult, StepResult } from './types.js' + +const execAsync = promisify(exec) + +export class TestExecutor { + private workingDir: string + + constructor(workingDir: string = process.cwd()) { + this.workingDir = workingDir + } + + async executeStep(command: string, timeout: number): Promise { + const startTime = Date.now() + let stdout = '' + let stderr = '' + let exitCode = 0 + + try { + const result = await execAsync(command, { + cwd: this.workingDir, + timeout, + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + shell: '/bin/bash' + }) + stdout = result.stdout + stderr = result.stderr + } catch (error: any) { + stdout = error.stdout || '' + stderr = error.stderr || error.message || 'Unknown error' + exitCode = error.code || 1 + } + + const duration = Date.now() - startTime + + return { + name: '', + command, + stdout, + stderr, + exitCode, + duration + } + } + + async executeTestCase(testCase: TestCase): Promise { + const startTime = Date.now() + const stepResults: StepResult[] = [] + + console.log(` Executing: ${testCase.id} - ${testCase.name}`) + + for (const step of testCase.steps) { + console.log(` Step: ${step.name}`) + + const timeout = step.timeout || testCase.timeout + const result = await this.executeStep(step.command, timeout) + result.name = step.name + + stepResults.push(result) + + // Log step result + if (result.exitCode === 0) { + console.log(` Exit: ${result.exitCode} (${result.duration}ms)`) + } else { + console.log(` Exit: ${result.exitCode} (FAILED, ${result.duration}ms)`) + } + } + + const totalDuration = Date.now() - startTime + + // Combine all logs + const logs = stepResults.map(r => { + return `=== Step: ${r.name} === +Command: ${r.command} +Exit Code: ${r.exitCode} +Duration: ${r.duration}ms + +STDOUT: +${r.stdout || '(empty)'} + +STDERR: +${r.stderr || '(empty)'} +` + }).join('\n' + '='.repeat(50) + '\n') + + return { + testCase, + steps: stepResults, + totalDuration, + logs + } + } + + async executeAll(testCases: TestCase[], concurrency: number = 1): Promise { + const results: TestResult[] = [] + + if (concurrency === 1) { + // Sequential execution + for (const tc of testCases) { + const result = await this.executeTestCase(tc) + results.push(result) + } + } else { + // Parallel execution with p-limit + const pLimit = (await import('p-limit')).default + const limit = pLimit(concurrency) + + const promises = testCases.map(tc => + limit(() => this.executeTestCase(tc)) + ) + + const parallelResults = await Promise.all(promises) + results.push(...parallelResults) + } + + return results + } +} diff --git a/tests/src/judge.ts b/tests/src/judge.ts new file mode 100644 index 00000000..78ac4489 --- /dev/null +++ b/tests/src/judge.ts @@ -0,0 +1,146 @@ +import axios from 'axios' +import { TestResult, Judgment } from './types.js' + +export class LLMJudge { + private ollamaUrl: string + private model: string + private batchSize: number + + constructor(ollamaUrl: string = 'http://localhost:11434', model: string = 'gemma3:4b') { + this.ollamaUrl = ollamaUrl + this.model = model + this.batchSize = 5 // Judge 5 tests per LLM call + } + + private buildPrompt(results: TestResult[]): string { + const testsSection = results.map((r, i) => { + return ` +### Test ${i + 1}: ${r.testCase.id} - ${r.testCase.name} + +**Criteria:** +${r.testCase.criteria} + +**Execution Logs:** +\`\`\` +${r.logs.substring(0, 3000)}${r.logs.length > 3000 ? '\n... (truncated)' : ''} +\`\`\` +` + }).join('\n---\n') + + return `You are a test evaluation judge. Analyze the following test results and determine if each test passed or failed based on the criteria provided. + +For each test, examine: +1. The expected criteria +2. The actual execution logs (stdout, stderr, exit codes) +3. Whether the output meets the criteria (use fuzzy matching for AI outputs) + +${testsSection} + +Respond with a JSON array containing one object per test: +[ + {"testId": "TC-XXX-001", "pass": true, "reason": "Brief explanation"}, + {"testId": "TC-XXX-002", "pass": false, "reason": "Brief explanation"} +] + +Important: +- For AI-generated text, accept reasonable variations (e.g., "4", "four", "The answer is 4" are all valid for math questions) +- For build/runtime tests, check exit codes and absence of error messages +- Be lenient with formatting differences, focus on semantic correctness + +Respond ONLY with the JSON array, no other text.` + } + + async judgeResults(results: TestResult[]): Promise { + const allJudgments: Judgment[] = [] + + // Process in batches + for (let i = 0; i < results.length; i += this.batchSize) { + const batch = results.slice(i, i + this.batchSize) + console.log(` Judging batch ${Math.floor(i / this.batchSize) + 1}/${Math.ceil(results.length / this.batchSize)}...`) + + try { + const judgments = await this.judgeBatch(batch) + allJudgments.push(...judgments) + } catch (error) { + console.error(` Failed to judge batch:`, error) + // Mark all tests in batch as failed + for (const r of batch) { + allJudgments.push({ + testId: r.testCase.id, + pass: false, + reason: 'LLM judgment failed: ' + String(error) + }) + } + } + } + + return allJudgments + } + + private async judgeBatch(results: TestResult[]): Promise { + const prompt = this.buildPrompt(results) + + const response = await axios.post(`${this.ollamaUrl}/api/generate`, { + model: this.model, + prompt, + stream: false, + options: { + temperature: 0.1, // Low temperature for consistent judging + num_predict: 1000 + } + }, { + timeout: 120000 // 2 minute timeout + }) + + const responseText = response.data.response + + // Extract JSON from response + const jsonMatch = responseText.match(/\[[\s\S]*\]/) + if (!jsonMatch) { + throw new Error('No JSON array found in LLM response') + } + + try { + const judgments = JSON.parse(jsonMatch[0]) as Judgment[] + + // Validate and fill missing + const resultIds = results.map(r => r.testCase.id) + const judgedIds = new Set(judgments.map(j => j.testId)) + + // Add missing judgments + for (const id of resultIds) { + if (!judgedIds.has(id)) { + judgments.push({ + testId: id, + pass: false, + reason: 'No judgment provided by LLM' + }) + } + } + + return judgments + } catch (parseError) { + throw new Error(`Failed to parse LLM response: ${responseText.substring(0, 200)}`) + } + } + + // Fallback: Simple rule-based judgment (no LLM) + simpleJudge(result: TestResult): Judgment { + const allStepsPassed = result.steps.every(s => s.exitCode === 0) + + if (allStepsPassed) { + return { + testId: result.testCase.id, + pass: true, + reason: 'All steps completed with exit code 0' + } + } else { + const failedSteps = result.steps.filter(s => s.exitCode !== 0) + return { + testId: result.testCase.id, + pass: false, + reason: `Steps failed: ${failedSteps.map(s => s.name).join(', ')}` + } + } + } +} diff --git a/tests/src/loader.ts b/tests/src/loader.ts new file mode 100644 index 00000000..778ca642 --- /dev/null +++ b/tests/src/loader.ts @@ -0,0 +1,91 @@ +import { readFileSync } from 'fs' +import { glob } from 'glob' +import yaml from 'js-yaml' +import path from 'path' +import { TestCase } from './types.js' + +export class TestLoader { + private testcasesDir: string + + constructor(testcasesDir: string = './testcases') { + this.testcasesDir = testcasesDir + } + + async loadAll(): Promise { + const pattern = path.join(this.testcasesDir, '**/*.yml') + const files = await glob(pattern) + + const testCases: TestCase[] = [] + + for (const file of files) { + try { + const content = readFileSync(file, 'utf-8') + const testCase = yaml.load(content) as TestCase + + // Set defaults + testCase.timeout = testCase.timeout || 60000 + testCase.dependencies = testCase.dependencies || [] + testCase.priority = testCase.priority || 1 + + testCases.push(testCase) + } catch (error) { + console.error(`Failed to load ${file}:`, error) + } + } + + return testCases + } + + async loadBySuite(suite: string): Promise { + const all = await this.loadAll() + return all.filter(tc => tc.suite === suite) + } + + async loadById(id: string): Promise { + const all = await this.loadAll() + return all.find(tc => tc.id === id) + } + + // Sort test cases by dependencies (topological sort) + sortByDependencies(testCases: TestCase[]): TestCase[] { + const sorted: TestCase[] = [] + const visited = new Set() + const idMap = new Map(testCases.map(tc => [tc.id, tc])) + + const visit = (tc: TestCase) => { + if (visited.has(tc.id)) return + visited.add(tc.id) + + // Visit dependencies first + for (const depId of tc.dependencies) { + const dep = idMap.get(depId) + if (dep) visit(dep) + } + + sorted.push(tc) + } + + // Sort by priority first, then by dependencies + const byPriority = [...testCases].sort((a, b) => a.priority - b.priority) + for (const tc of byPriority) { + visit(tc) + } + + return sorted + } + + // Group test cases by suite for parallel execution + groupBySuite(testCases: TestCase[]): Map { + const groups = new Map() + + for (const tc of testCases) { + const suite = tc.suite + if (!groups.has(suite)) { + groups.set(suite, []) + } + groups.get(suite)!.push(tc) + } + + return groups + } +} diff --git a/tests/src/reporter.ts b/tests/src/reporter.ts new file mode 100644 index 00000000..cc0f6012 --- /dev/null +++ b/tests/src/reporter.ts @@ -0,0 +1,138 @@ +import axios from 'axios' +import { TestReport, Judgment, TestResult } from './types.js' + +export class Reporter { + // Console reporter + static toConsole(reports: TestReport[]): void { + console.log('\n' + '='.repeat(60)) + console.log('TEST RESULTS') + console.log('='.repeat(60)) + + const passed = reports.filter(r => r.pass) + const failed = reports.filter(r => !r.pass) + + for (const report of reports) { + const status = report.pass ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m' + console.log(`[${status}] ${report.testId}: ${report.name}`) + console.log(` Reason: ${report.reason}`) + console.log(` Duration: ${report.duration}ms`) + } + + console.log('\n' + '-'.repeat(60)) + console.log(`Total: ${reports.length} | Passed: ${passed.length} | Failed: ${failed.length}`) + console.log('='.repeat(60)) + } + + // JSON reporter + static toJSON(reports: TestReport[]): string { + return JSON.stringify({ + summary: { + total: reports.length, + passed: reports.filter(r => r.pass).length, + failed: reports.filter(r => !r.pass).length, + timestamp: new Date().toISOString() + }, + results: reports + }, null, 2) + } + + // JUnit XML reporter (for CI/CD integration) + static toJUnit(reports: TestReport[]): string { + const escapeXml = (s: string) => s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + + const testcases = reports.map(r => { + if (r.pass) { + return ` ` + } else { + return ` + ${escapeXml(r.logs.substring(0, 1000))} + ` + } + }).join('\n') + + const failures = reports.filter(r => !r.pass).length + const time = reports.reduce((sum, r) => sum + r.duration, 0) / 1000 + + return ` + +${testcases} +` + } + + // Combine results and judgments into reports + static createReports(results: TestResult[], judgments: Judgment[]): TestReport[] { + const judgmentMap = new Map(judgments.map(j => [j.testId, j])) + + return results.map(result => { + const judgment = judgmentMap.get(result.testCase.id) + + return { + testId: result.testCase.id, + name: result.testCase.name, + suite: result.testCase.suite, + pass: judgment?.pass ?? false, + reason: judgment?.reason ?? 'No judgment', + duration: result.totalDuration, + logs: result.logs + } + }) + } +} + +// TestLink reporter +export class TestLinkReporter { + private url: string + private apiKey: string + + constructor(url: string, apiKey: string) { + this.url = url + this.apiKey = apiKey + } + + async reportResults( + reports: TestReport[], + planId: string, + buildId: string + ): Promise { + console.log('\nReporting to TestLink...') + + for (const report of reports) { + try { + await this.reportTestExecution(report, planId, buildId) + console.log(` Reported: ${report.testId}`) + } catch (error) { + console.error(` Failed to report ${report.testId}:`, error) + } + } + } + + private async reportTestExecution( + report: TestReport, + planId: string, + buildId: string + ): Promise { + // Extract numeric test case ID from external ID (e.g., "ollama37-8" -> need internal ID) + // This would need to be mapped from TestLink + + const status = report.pass ? 'p' : 'f' // p=passed, f=failed, b=blocked + + // Note: This uses the TestLink XML-RPC API + // In practice, you'd use the testlink-mcp or direct API calls + const payload = { + devKey: this.apiKey, + testcaseexternalid: report.testId, + testplanid: planId, + buildid: buildId, + status, + notes: `${report.reason}\n\nDuration: ${report.duration}ms\n\nLogs:\n${report.logs.substring(0, 4000)}` + } + + // For now, just log - actual implementation would call TestLink API + console.log(` Would report: ${report.testId} = ${status}`) + } +} diff --git a/tests/src/types.ts b/tests/src/types.ts new file mode 100644 index 00000000..f07badcf --- /dev/null +++ b/tests/src/types.ts @@ -0,0 +1,66 @@ +// Test case definition +export interface TestStep { + name: string + command: string + timeout?: number +} + +export interface TestCase { + id: string + name: string + suite: string + priority: number + timeout: number + dependencies: string[] + steps: TestStep[] + criteria: string +} + +// Execution results +export interface StepResult { + name: string + command: string + stdout: string + stderr: string + exitCode: number + duration: number +} + +export interface TestResult { + testCase: TestCase + steps: StepResult[] + totalDuration: number + logs: string +} + +// LLM judgment +export interface Judgment { + testId: string + pass: boolean + reason: string +} + +// Final report +export interface TestReport { + testId: string + name: string + suite: string + pass: boolean + reason: string + duration: number + logs: string +} + +// Runner options +export interface RunnerOptions { + suite?: string + id?: string + workers: number + dryRun: boolean + output: 'console' | 'json' | 'junit' + reportTestlink: boolean + ollamaUrl: string + ollamaModel: string + testlinkUrl: string + testlinkApiKey: string +} diff --git a/tests/testcases/build/TC-BUILD-001.yml b/tests/testcases/build/TC-BUILD-001.yml new file mode 100644 index 00000000..394f2a11 --- /dev/null +++ b/tests/testcases/build/TC-BUILD-001.yml @@ -0,0 +1,31 @@ +id: TC-BUILD-001 +name: Builder Image Verification +suite: build +priority: 1 +timeout: 120000 + +dependencies: [] + +steps: + - name: Check image exists + command: docker images ollama37-builder:latest --format '{{.Repository}}:{{.Tag}}' + + - name: Verify CUDA toolkit + command: docker run --rm ollama37-builder:latest nvcc --version + + - name: Verify GCC version + command: docker run --rm ollama37-builder:latest gcc --version | head -1 + + - name: Verify Go version + command: docker run --rm ollama37-builder:latest go version + +criteria: | + All commands should succeed (exit code 0). + + Expected outputs: + - Image exists: should show "ollama37-builder:latest" + - CUDA: should show version 11.4 (accept 11.4.x) + - GCC: should show version 10 (accept GCC 10.x) + - Go: should show version 1.25 or higher + + Accept minor version variations. Focus on major versions being correct. diff --git a/tests/testcases/build/TC-BUILD-002.yml b/tests/testcases/build/TC-BUILD-002.yml new file mode 100644 index 00000000..adeef779 --- /dev/null +++ b/tests/testcases/build/TC-BUILD-002.yml @@ -0,0 +1,27 @@ +id: TC-BUILD-002 +name: Runtime Image Build +suite: build +priority: 2 +timeout: 900000 + +dependencies: + - TC-BUILD-001 + +steps: + - name: Build runtime image + command: cd docker && make build-runtime-no-cache 2>&1 | tail -50 + timeout: 900000 + + - name: Verify runtime image exists + command: docker images ollama37:latest --format '{{.Repository}}:{{.Tag}} {{.Size}}' + +criteria: | + The runtime Docker image should build successfully from GitHub source. + + Expected: + - Build completes without fatal errors + - Final output should mention "successfully" or similar completion message + - Runtime image "ollama37:latest" should exist after build + - Image size should be substantial (>10GB is expected due to CUDA) + + Accept build warnings. Only fail on actual build errors. diff --git a/tests/testcases/build/TC-BUILD-003.yml b/tests/testcases/build/TC-BUILD-003.yml new file mode 100644 index 00000000..9b71eced --- /dev/null +++ b/tests/testcases/build/TC-BUILD-003.yml @@ -0,0 +1,25 @@ +id: TC-BUILD-003 +name: Image Size Validation +suite: build +priority: 3 +timeout: 30000 + +dependencies: + - TC-BUILD-002 + +steps: + - name: Check builder image size + command: docker images ollama37-builder:latest --format '{{.Size}}' + + - name: Check runtime image size + command: docker images ollama37:latest --format '{{.Size}}' + +criteria: | + Docker images should be within expected size ranges. + + Expected: + - Builder image: 10GB to 20GB (contains CUDA, GCC, CMake, Go) + - Runtime image: 15GB to 25GB (contains builder + compiled ollama) + + These are large images due to CUDA toolkit and build tools. + Accept sizes within reasonable range of expectations. diff --git a/tests/testcases/inference/TC-INFERENCE-001.yml b/tests/testcases/inference/TC-INFERENCE-001.yml new file mode 100644 index 00000000..d0f752b2 --- /dev/null +++ b/tests/testcases/inference/TC-INFERENCE-001.yml @@ -0,0 +1,30 @@ +id: TC-INFERENCE-001 +name: Model Pull +suite: inference +priority: 1 +timeout: 600000 + +dependencies: + - TC-RUNTIME-003 + +steps: + - name: Check if model exists + command: docker exec ollama37 ollama list | grep -q "gemma3:4b" && echo "Model exists" || echo "Model not found" + + - name: Pull model if needed + command: docker exec ollama37 ollama list | grep -q "gemma3:4b" || docker exec ollama37 ollama pull gemma3:4b + timeout: 600000 + + - name: Verify model available + command: docker exec ollama37 ollama list + +criteria: | + The gemma3:4b model should be available for inference. + + Expected: + - Model is either already present or successfully downloaded + - "ollama list" shows gemma3:4b in the output + - No download errors + + Accept if model already exists (skip download). + Model size is ~3GB, download may take time. diff --git a/tests/testcases/inference/TC-INFERENCE-002.yml b/tests/testcases/inference/TC-INFERENCE-002.yml new file mode 100644 index 00000000..a4d783b0 --- /dev/null +++ b/tests/testcases/inference/TC-INFERENCE-002.yml @@ -0,0 +1,28 @@ +id: TC-INFERENCE-002 +name: Basic Inference +suite: inference +priority: 2 +timeout: 180000 + +dependencies: + - TC-INFERENCE-001 + +steps: + - name: Run simple math question + command: docker exec ollama37 ollama run gemma3:4b "What is 2+2? Answer with just the number." 2>&1 + timeout: 120000 + + - name: Check GPU memory usage + command: docker exec ollama37 nvidia-smi --query-compute-apps=pid,used_memory --format=csv 2>/dev/null || echo "No GPU processes" + +criteria: | + Basic inference should work on Tesla K80. + + Expected: + - Model responds to the math question + - Response should indicate "4" (accept variations: "4", "four", "The answer is 4", etc.) + - GPU memory should be allocated during inference + - No CUDA errors in output + + This is AI-generated output - accept reasonable variations. + Focus on the model producing a coherent response. diff --git a/tests/testcases/inference/TC-INFERENCE-003.yml b/tests/testcases/inference/TC-INFERENCE-003.yml new file mode 100644 index 00000000..810e0532 --- /dev/null +++ b/tests/testcases/inference/TC-INFERENCE-003.yml @@ -0,0 +1,34 @@ +id: TC-INFERENCE-003 +name: API Endpoint Test +suite: inference +priority: 3 +timeout: 120000 + +dependencies: + - TC-INFERENCE-001 + +steps: + - name: Test generate endpoint (non-streaming) + command: | + curl -s http://localhost:11434/api/generate \ + -d '{"model":"gemma3:4b","prompt":"Say hello in one word","stream":false}' \ + | head -c 500 + + - name: Test generate endpoint (streaming) + command: | + curl -s http://localhost:11434/api/generate \ + -d '{"model":"gemma3:4b","prompt":"Count from 1 to 3","stream":true}' \ + | head -5 + +criteria: | + Ollama REST API should handle inference requests. + + Expected for non-streaming: + - Returns JSON with "response" field + - Response contains some greeting (hello, hi, etc.) + + Expected for streaming: + - Returns multiple JSON lines + - Each line contains partial response + + Accept any valid JSON response. Content may vary. diff --git a/tests/testcases/inference/TC-INFERENCE-004.yml b/tests/testcases/inference/TC-INFERENCE-004.yml new file mode 100644 index 00000000..c2ee5e33 --- /dev/null +++ b/tests/testcases/inference/TC-INFERENCE-004.yml @@ -0,0 +1,32 @@ +id: TC-INFERENCE-004 +name: CUBLAS Fallback Verification +suite: inference +priority: 4 +timeout: 120000 + +dependencies: + - TC-INFERENCE-002 + +steps: + - name: Check for CUBLAS errors in logs + command: cd docker && docker compose logs 2>&1 | grep -i "CUBLAS_STATUS" | grep -v "SUCCESS" | head -10 || echo "No CUBLAS errors" + + - name: Check compute capability detection + command: cd docker && docker compose logs 2>&1 | grep -iE "compute|capability|cc.*3" | head -10 || echo "No compute capability logs" + + - name: Verify no GPU errors + command: cd docker && docker compose logs 2>&1 | grep -iE "error|fail" | grep -i gpu | head -10 || echo "No GPU errors" + +criteria: | + CUBLAS should work correctly on Tesla K80 using legacy fallback. + + Expected: + - No CUBLAS_STATUS_ARCH_MISMATCH errors + - No CUBLAS_STATUS_NOT_SUPPORTED errors + - Compute capability 3.7 may be mentioned in debug logs + - No fatal GPU-related errors + + The K80 uses legacy CUBLAS functions (cublasSgemmBatched) + instead of modern Ex variants. This should work transparently. + + Accept warnings. Only fail on actual CUBLAS errors. diff --git a/tests/testcases/runtime/TC-RUNTIME-001.yml b/tests/testcases/runtime/TC-RUNTIME-001.yml new file mode 100644 index 00000000..a8040f24 --- /dev/null +++ b/tests/testcases/runtime/TC-RUNTIME-001.yml @@ -0,0 +1,31 @@ +id: TC-RUNTIME-001 +name: Container Startup +suite: runtime +priority: 1 +timeout: 120000 + +dependencies: + - TC-BUILD-002 + +steps: + - name: Stop existing container + command: cd docker && docker compose down 2>/dev/null || true + + - name: Start container with GPU + command: cd docker && docker compose up -d + + - name: Wait for startup + command: sleep 15 + + - name: Check container status + command: cd docker && docker compose ps + +criteria: | + The ollama37 container should start successfully with GPU access. + + Expected: + - Container starts without errors + - docker compose ps shows container in "Up" state + - No "Exited" or "Restarting" status + + Accept startup warnings. Container should be running. diff --git a/tests/testcases/runtime/TC-RUNTIME-002.yml b/tests/testcases/runtime/TC-RUNTIME-002.yml new file mode 100644 index 00000000..ab0345e6 --- /dev/null +++ b/tests/testcases/runtime/TC-RUNTIME-002.yml @@ -0,0 +1,29 @@ +id: TC-RUNTIME-002 +name: GPU Detection +suite: runtime +priority: 2 +timeout: 60000 + +dependencies: + - TC-RUNTIME-001 + +steps: + - name: Check nvidia-smi inside container + command: docker exec ollama37 nvidia-smi + + - name: Check CUDA libraries + command: docker exec ollama37 ldconfig -p | grep -i cuda | head -5 + + - name: Check Ollama GPU detection + command: cd docker && docker compose logs 2>&1 | grep -i gpu | head -10 + +criteria: | + Tesla K80 GPU should be detected inside the container. + + Expected: + - nvidia-smi shows Tesla K80 GPU(s) + - Driver version 470.x (or compatible) + - CUDA libraries are available (libcuda, libcublas, etc.) + - Ollama logs mention GPU detection + + The K80 has 12GB VRAM per GPU. Accept variations in reported memory. diff --git a/tests/testcases/runtime/TC-RUNTIME-003.yml b/tests/testcases/runtime/TC-RUNTIME-003.yml new file mode 100644 index 00000000..ac2bfc1f --- /dev/null +++ b/tests/testcases/runtime/TC-RUNTIME-003.yml @@ -0,0 +1,39 @@ +id: TC-RUNTIME-003 +name: Health Check +suite: runtime +priority: 3 +timeout: 180000 + +dependencies: + - TC-RUNTIME-001 + +steps: + - name: Wait for health check + command: | + for i in {1..30}; do + STATUS=$(docker inspect ollama37 --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") + echo "Health status: $STATUS (attempt $i/30)" + if [ "$STATUS" = "healthy" ]; then + echo "Container is healthy" + exit 0 + fi + sleep 5 + done + echo "Health check timeout" + exit 1 + + - name: Test API endpoint + command: curl -s http://localhost:11434/api/tags + + - name: Check Ollama version + command: docker exec ollama37 ollama --version + +criteria: | + Ollama server should be healthy and API responsive. + + Expected: + - Container health status becomes "healthy" + - /api/tags endpoint returns JSON response (even if empty models) + - ollama --version shows version information + + Accept any valid JSON response from API. Version format may vary. diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 00000000..52f21044 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}