diff --git a/.github/workflows/.disabled/pre-commit.yml.disabled b/.github/workflows/.disabled/pre-commit.yml.disabled new file mode 100644 index 0000000..cd21b6e --- /dev/null +++ b/.github/workflows/.disabled/pre-commit.yml.disabled @@ -0,0 +1,54 @@ +# ----------------------------------------------------------------------------- +# DISABLED WORKFLOW (kept for reference) +# +# This is the default pre-commit workflow intended to run on GitHub-hosted +# `ubuntu-latest` runners. It has been disabled by renaming the file to end in +# `.yml.disabled` so GitHub Actions will not discover or execute it. The active +# workflow used by this repo is `pre-commit.yml`, which targets the SEMOSS +# self-hosted CodeBuild runner (`codebuild-semoss-github-runner-...`). +# +# To restore this default workflow (e.g., if migrating back to GitHub-hosted +# runners), rename this file back to `pre-commit-default.yml` and remove or +# rename the self-hosted variant. +# ----------------------------------------------------------------------------- + +name: Pre-commit + +on: + pull_request: + workflow_dispatch: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install dependencies and run frontend linting + run: | + if [ -d "client" ]; then + pnpm install + pnpm exec biome format --write && pnpm exec biome lint + else + echo "No client directory found, skipping frontend linting" + fi + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12.9' + + # Official pre-commit action handles caching automatically + - uses: pre-commit/action@v3.0.1 + with: + extra_args: --all-files --show-diff-on-failure diff --git a/.github/workflows/.disabled/unit-test.yml.disabled b/.github/workflows/.disabled/unit-test.yml.disabled new file mode 100644 index 0000000..b54c8c1 --- /dev/null +++ b/.github/workflows/.disabled/unit-test.yml.disabled @@ -0,0 +1,73 @@ +# ----------------------------------------------------------------------------- +# DISABLED WORKFLOW (kept for reference) +# +# This is the default unit-test workflow intended to run on GitHub-hosted +# `ubuntu-latest` runners. It has been disabled by renaming the file to end in +# `.yml.disabled` so GitHub Actions will not discover or execute it. The active +# workflow used by this repo is `unit-test.yml`, which targets the SEMOSS +# self-hosted CodeBuild runner (`codebuild-semoss-github-runner-...`). +# +# To restore this default workflow (e.g., if migrating back to GitHub-hosted +# runners), rename this file back to `unit-test-default.yml` and remove or +# rename the self-hosted variant. +# ----------------------------------------------------------------------------- + +name: Run Unit Tests + +on: + pull_request: + workflow_dispatch: + inputs: + snapshot: + description: 'CI version snapshot' + required: true + default: '5.3.0-SNAPSHOT' + type: choice + options: + - '5.3.0-SNAPSHOT' + # Add other snapshot versions as needed here! + clean_install: + description: 'Run `mvn clean install -U` to force-update dependencies' + required: true + default: false + type: boolean + +jobs: + test: + name: Run Test Suite + runs-on: ubuntu-latest + container: maven:3.9.9-amazoncorretto-21-debian + env: + SNAPSHOT: ${{ inputs.snapshot || '5.3.0-SNAPSHOT' }} + CLEAN_INSTALL: ${{ inputs.clean_install || 'false' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Maven packages + id: cache-maven + uses: actions/cache@v4 + with: + path: /root/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}-${{ env.SNAPSHOT }} + restore-keys: | + ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}- + ${{ runner.os }}-maven- + + - name: Maven Install + if: steps.cache-maven.outputs.cache-hit != 'true' || env.CLEAN_INSTALL == 'true' + run: | + echo "Clean install: ${CLEAN_INSTALL}" + echo "Version snapshot: ${SNAPSHOT}" + if [ "${CLEAN_INSTALL}" = "true" ]; then + echo "Running mvn clean install with -U flag" + mvn -B clean install -U -DskipTests=true -Dci.version="${SNAPSHOT}" + else + echo "Running mvn install without -U flag" + mvn -B install -DskipTests=true -Dci.version="${SNAPSHOT}" + fi + + - name: Run Unit Tests + run: mvn -B ${{ steps.cache-maven.outputs.cache-hit == 'true' && '-o' || '' }} test -Dci.version="${SNAPSHOT}" + \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..18f7d0e --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,88 @@ +name: Pre-commit + +on: + pull_request: + workflow_dispatch: + +jobs: + pre-commit: + runs-on: codebuild-semoss-github-runner-${{ github.run_id }}-${{ github.run_attempt }} + container: public.ecr.aws/docker/library/python:3.12-bookworm + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'pnpm' + cache-dependency-path: 'client/pnpm-lock.yaml' + + - name: Install dependencies and run frontend linting + run: | + if [ -d "client" ]; then + pnpm install + pnpm exec biome format --write && pnpm exec biome lint + else + echo "No client directory found, skipping frontend linting" + fi + + # Python 3.12 is provided by the container + - name: Compute cache keys + id: keys + run: | + echo "pre-commit=$(sha256sum .pre-commit-config.yaml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache pre-commit hook environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-py3.12-${{ steps.keys.outputs.pre-commit }} + restore-keys: | + pre-commit-py3.12- + + - name: Cache uv downloads + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + ~/.local/share/uv + key: uv-py3.12-${{ runner.os }} + restore-keys: | + uv-py3.12- + + - name: Cache OpenJDK 17 + id: cache-jdk + uses: actions/cache@v4 + with: + path: /usr/lib/jvm/java-17-openjdk-amd64 + key: jdk-17-bookworm-${{ runner.os }} + + - name: Install OpenJDK 17 headless (required by pretty-format-java hook) + if: steps.cache-jdk.outputs.cache-hit != 'true' + run: | + apt-get update + apt-get install -y --no-install-recommends openjdk-17-jre-headless + + - name: Set up Java environment + run: | + echo "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" >> "$GITHUB_ENV" + echo "/usr/lib/jvm/java-17-openjdk-amd64/bin" >> "$GITHUB_PATH" + java -version + + - name: Install uv + run: | + if ! command -v uv >/dev/null 2>&1; then + curl -LsSf https://astral.sh/uv/install.sh | sh + fi + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Run pre-commit + run: | + uv tool install pre-commit + uv tool run pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..62646a4 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,73 @@ +name: Run Unit Tests + +on: + pull_request: + workflow_dispatch: + inputs: + snapshot: + description: 'CI version snapshot' + required: true + default: '5.3.0-SNAPSHOT' + type: choice + options: + - '5.3.0-SNAPSHOT' + # Add other snapshot versions as needed here! + clean_install: + description: 'Run `mvn clean install -U` to force-update dependencies' + required: true + default: false + type: boolean + +jobs: + test: + name: Run Test Suite + runs-on: codebuild-semoss-github-runner-${{ github.run_id }}-${{ github.run_attempt }} + container: public.ecr.aws/docker/library/maven:3.9.9-amazoncorretto-21-debian + env: + SNAPSHOT: ${{ inputs.snapshot || '5.3.0-SNAPSHOT' }} + CLEAN_INSTALL: ${{ inputs.clean_install || 'false' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Computes a cache key from the root pom.xml. This project is a single-module + # Maven project, so hashing the root pom.xml alone is sufficient. + # + # For a multi-module Maven project, hash every pom.xml in the tree instead: + # run: echo "hash=$(find . -name pom.xml -not -path '*/target/*' -print0 \ + # | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + # + # Note: we compute the hash in a shell step instead of using the built-in + # hashFiles() expression because hashFiles() fails on this self-hosted + # CodeBuild runner due to a workspace-path resolution issue. + - name: Compute pom.xml hash + id: pom-hash + run: echo "hash=$(sha256sum pom.xml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + + - name: Cache Maven packages + id: cache-maven + uses: actions/cache@v4 + with: + path: /root/.m2/repository + key: ${{ runner.os }}-maven-${{ steps.pom-hash.outputs.hash }}-${{ env.SNAPSHOT }} + restore-keys: | + ${{ runner.os }}-maven-${{ steps.pom-hash.outputs.hash }}- + ${{ runner.os }}-maven- + + - name: Maven Install + if: steps.cache-maven.outputs.cache-hit != 'true' || env.CLEAN_INSTALL == 'true' + run: | + echo "Clean install: ${CLEAN_INSTALL}" + echo "Version snapshot: ${SNAPSHOT}" + if [ "${CLEAN_INSTALL}" = "true" ]; then + echo "Running mvn clean install with -U flag" + mvn -B clean install -U -DskipTests=true -Dci.version="${SNAPSHOT}" + else + echo "Running mvn install without -U flag" + mvn -B install -DskipTests=true -Dci.version="${SNAPSHOT}" + fi + + - name: Run Unit Tests + run: mvn -B ${{ steps.cache-maven.outputs.cache-hit == 'true' && '-o' || '' }} test -Dci.version="${SNAPSHOT}" + \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..6e3c7d8 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,2 @@ +# Run pre-commit hooks for commit-msg stage +pre-commit run --hook-stage commit-msg --commit-msg-filename "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..d8f48dd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,12 @@ +# Lint FE code +echo "Formatting and linting FE code..." +pnpm exec lint-staged + +# Capture list of staged files +STAGED_FILES=$(git diff --name-only --cached) + +# Format BE code +echo "Formatting BE code..." +pre-commit run --all-files + +# You may want to consider also running `pre-commit autoupdate` periodically to keep hooks updated. \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4bbdd52 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,65 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: end-of-file-fixer + files: '^(java/.*\.java|py.*\.py)$' + - id: trailing-whitespace + files: '^(java/.*\.java|py.*\.py)$' + - id: mixed-line-ending + args: ['--fix=auto'] + files: '^(java/.*\.java|py.*\.py)$' + - id: check-yaml + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.15.0 + hooks: + - id: pretty-format-java + args: [--autofix, --google-java-formatter-version=1.28.0] + files: '^java/.*\.java$' + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.10.0 + hooks: + - id: commitizen + stages: [commit-msg] + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + language_version: python3 + files: '^py.*\.py$' + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ['--profile', 'black'] + files: '^py.*\.py$' + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.18.2 + hooks: + - id: mypy + files: '\.py$' + args: [ + "--ignore-missing-imports", + "--show-error-codes", + "--explicit-package-bases", + "--allow-untyped-defs", + "--allow-incomplete-defs", + "--allow-untyped-calls", + "--no-strict-optional", + "--disable-error-code=misc", + "--disable-error-code=no-any-return", + "--disable-error-code=name-defined", + "--disable-error-code=attr-defined" + ] + additional_dependencies: [ # Continue to add dependencies here that have mypy extensions, such as pydantic + "types-requests", + "types-toml", + "pandas-stubs", + "numpy" + ] + - repo: https://github.com/pamoller/xmlformatter + rev: v0.2.8 + hooks: + - id: xml-formatter + files: '^.*\.xml$' diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..ae19203 --- /dev/null +++ b/biome.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.11/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "lineEnding": "lf", + "indentWidth": 4 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noUselessFragments": "error" + }, + "correctness": { + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useExhaustiveDependencies": { + "fix": "none", + "level": "error", + "options": {} + } + }, + "style": { + "useConst": "error" + }, + "suspicious": { + "noExplicitAny": "error", + "noConsole": "warn", + "noVar": "error" + } + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, + "files": { + "includes": [ + "client/**/*", + "!node_modules/**/*", + "**/*.{js,ts,mjs,cjs,jsx,tsx,json,css,scss,md,mdx,html,yaml}", + "!.history/*" + ] + }, + "overrides": [ + { + "includes": ["**/*d.ts"], + "linter": { + "rules": { + "correctness": { + "noUnusedVariables": "off" + } + } + } + } + ] +} diff --git a/client/package.json b/client/package.json index a9a84fb..bc782bb 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,8 @@ "private": true, "scripts": { "build": "vite build", - "dev": "vite" + "dev": "vite", + "fix": "pnpm --dir .. fix" }, "dependencies": { "@radix-ui/react-label": "^2.1.8", diff --git a/mcp/py_mcp.json b/mcp/py_mcp.json index 6cc9206..4bbf814 100644 --- a/mcp/py_mcp.json +++ b/mcp/py_mcp.json @@ -1,68 +1,64 @@ { - "_meta": { - "last_modified_date": "2026-04-10", - "file_last_modified_date": "2026-04-10", - "source_file": "/Users/tevanburen/Documents/SEMOSS/workspace/Semoss/project/Blank Canvas__9447b875-266e-4814-a1de-c840ff88401a/app_root/version/assets/py/mcp_driver.py" - }, - "tools": [ - { - "name": "fahrenheit_to_celsius", - "title": "Fahrenheit To Celsius", - "description": "Convert a temperature from Fahrenheit to Celsius.", - "inputSchema": { - "properties": { - "temperature_f": { - "title": "Temperature F", - "description": "No docstring present or unable to parse docstring from function", - "type": "number" - } - }, - "required": [ - "temperature_f" - ], - "title": "Fahrenheit To Celsius Arguments", - "type": "object" - }, - "_meta": { - "generated_on": "2026-04-10", - "SMSS_MCP_EXECUTION": "auto", - "SMSS_MCP_UI": { - "displayLocation": "inline", - "loadingMessage": "Converting temperature...", - "resourceURI": "/#/" - }, - "SMSS_FUNCTION_NAME": "fahrenheit_to_celsius" - }, - "_type": "python" - }, - { - "name": "celsius_to_fahrenheit", - "title": "Celsius To Fahrenheit", - "description": "Convert a temperature from Celsius to Fahrenheit.", - "inputSchema": { - "properties": { - "temperature_c": { - "title": "Temperature C", - "description": "No docstring present or unable to parse docstring from function", - "type": "number" - } - }, - "required": [ - "temperature_c" - ], - "title": "Celsius To Fahrenheit Arguments", - "type": "object" - }, - "_meta": { - "generated_on": "2026-04-10", - "SMSS_MCP_EXECUTION": "auto", - "SMSS_MCP_UI": { - "displayLocation": "inline", - "loadingMessage": "Converting temperature..." - }, - "SMSS_FUNCTION_NAME": "celsius_to_fahrenheit" - }, - "_type": "python" - } - ] -} \ No newline at end of file + "_meta": { + "last_modified_date": "2026-04-10", + "file_last_modified_date": "2026-04-10", + "source_file": "/Users/tevanburen/Documents/SEMOSS/workspace/Semoss/project/Blank Canvas__9447b875-266e-4814-a1de-c840ff88401a/app_root/version/assets/py/mcp_driver.py" + }, + "tools": [ + { + "name": "fahrenheit_to_celsius", + "title": "Fahrenheit To Celsius", + "description": "Convert a temperature from Fahrenheit to Celsius.", + "inputSchema": { + "properties": { + "temperature_f": { + "title": "Temperature F", + "description": "No docstring present or unable to parse docstring from function", + "type": "number" + } + }, + "required": ["temperature_f"], + "title": "Fahrenheit To Celsius Arguments", + "type": "object" + }, + "_meta": { + "generated_on": "2026-04-10", + "SMSS_MCP_EXECUTION": "auto", + "SMSS_MCP_UI": { + "displayLocation": "inline", + "loadingMessage": "Converting temperature...", + "resourceURI": "/#/" + }, + "SMSS_FUNCTION_NAME": "fahrenheit_to_celsius" + }, + "_type": "python" + }, + { + "name": "celsius_to_fahrenheit", + "title": "Celsius To Fahrenheit", + "description": "Convert a temperature from Celsius to Fahrenheit.", + "inputSchema": { + "properties": { + "temperature_c": { + "title": "Temperature C", + "description": "No docstring present or unable to parse docstring from function", + "type": "number" + } + }, + "required": ["temperature_c"], + "title": "Celsius To Fahrenheit Arguments", + "type": "object" + }, + "_meta": { + "generated_on": "2026-04-10", + "SMSS_MCP_EXECUTION": "auto", + "SMSS_MCP_UI": { + "displayLocation": "inline", + "loadingMessage": "Converting temperature..." + }, + "SMSS_FUNCTION_NAME": "celsius_to_fahrenheit" + }, + "_type": "python" + } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8e9638f --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "private": true, + "scripts": { + "fix": "biome format --write && biome lint && pre-commit run --all-files || pre-commit run --all-files", + "dev": "pnpm --dir client dev" + }, + "devDependencies": { + "@biomejs/biome": "2.4.11", + "http-server": "^14.1.1", + "husky": "^9.1.7", + "lint-staged": "^16.4.0" + }, + "lint-staged": { + "**/*.{js,jsx,ts,tsx,json,html,css}": [ + "biome format --write", + "biome lint" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..561f5de --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,745 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@biomejs/biome': + specifier: 2.4.11 + version: 2.4.11 + http-server: + specifier: ^14.1.1 + version: 14.1.1 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.4.0 + version: 16.4.0 + +packages: + + '@biomejs/biome@2.4.11': + resolution: {integrity: sha512-nWxHX8tf3Opb/qRgZpBbsTOqOodkbrkJ7S+JxJAruxOReaDPPmPuLBAGQ8vigyUgo0QBB+oQltNEAvalLcjggA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.11': + resolution: {integrity: sha512-wOt+ed+L2dgZanWyL6i29qlXMc088N11optzpo10peayObBaAshbTcxKUchzEMp9QSY8rh5h6VfAFE3WTS1rqg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.11': + resolution: {integrity: sha512-gZ6zR8XmZlExfi/Pz/PffmdpWOQ8Qhy7oBztgkR8/ylSRyLwfRPSadmiVCV8WQ8PoJ2MWUy2fgID9zmtgUUJmw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.11': + resolution: {integrity: sha512-+Sbo1OAmlegtdwqFE8iOxFIWLh1B3OEgsuZfBpyyN/kWuqZ8dx9ZEes6zVnDMo+zRHF2wLynRVhoQmV7ohxl2Q==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.11': + resolution: {integrity: sha512-avdJaEElXrKceK0va9FkJ4P5ci3N01TGkc6ni3P8l3BElqbOz42Wg2IyX3gbh0ZLEd4HVKEIrmuVu/AMuSeFFA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.11': + resolution: {integrity: sha512-bexd2IklK7ZgPhrz6jXzpIL6dEAH9MlJU1xGTrypx+FICxrXUp4CqtwfiuoDKse+UlgAlWtzML3jrMqeEAHEhA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.11': + resolution: {integrity: sha512-TagWV0iomp5LnEnxWFg4nQO+e52Fow349vaX0Q/PIcX6Zhk4GGBgp3qqZ8PVkpC+cuehRctMf3+6+FgQ8jCEFQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.11': + resolution: {integrity: sha512-RJhaTnY8byzxDt4bDVb7AFPHkPcjOPK3xBip4ZRTrN3TEfyhjLRm3r3mqknqydgVTB74XG8l4jMLwEACEeihVg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.11': + resolution: {integrity: sha512-A8D3JM/00C2KQgUV3oj8Ba15EHEYwebAGCy5Sf9GAjr5Y3+kJIYOiESoqRDeuRZueuMdCsbLZIUqmPhpYXJE9A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + corser@2.0.1: + resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} + engines: {node: '>= 0.4.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-server@14.1.1: + resolution: {integrity: sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==} + engines: {node: '>=12'} + hasBin: true + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + portfinder@1.0.38: + resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} + engines: {node: '>= 10.12'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-compare@3.0.1: + resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + union@0.5.0: + resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} + engines: {node: '>= 0.8.0'} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + '@biomejs/biome@2.4.11': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.11 + '@biomejs/cli-darwin-x64': 2.4.11 + '@biomejs/cli-linux-arm64': 2.4.11 + '@biomejs/cli-linux-arm64-musl': 2.4.11 + '@biomejs/cli-linux-x64': 2.4.11 + '@biomejs/cli-linux-x64-musl': 2.4.11 + '@biomejs/cli-win32-arm64': 2.4.11 + '@biomejs/cli-win32-x64': 2.4.11 + + '@biomejs/cli-darwin-arm64@2.4.11': + optional: true + + '@biomejs/cli-darwin-x64@2.4.11': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.11': + optional: true + + '@biomejs/cli-linux-arm64@2.4.11': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.11': + optional: true + + '@biomejs/cli-linux-x64@2.4.11': + optional: true + + '@biomejs/cli-win32-arm64@2.4.11': + optional: true + + '@biomejs/cli-win32-x64@2.4.11': + optional: true + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + async@3.2.6: {} + + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + commander@14.0.3: {} + + corser@2.0.1: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@10.6.0: {} + + environment@1.1.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + + follow-redirects@1.16.0: {} + + function-bind@1.1.2: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-encoding-sniffer@3.0.0: + dependencies: + whatwg-encoding: 2.0.0 + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-server@14.1.1: + dependencies: + basic-auth: 2.0.1 + chalk: 4.1.2 + corser: 2.0.1 + he: 1.2.0 + html-encoding-sniffer: 3.0.0 + http-proxy: 1.18.1 + mime: 1.6.0 + minimist: 1.2.8 + opener: 1.5.2 + portfinder: 1.0.38 + secure-compare: 3.0.1 + union: 0.5.0 + url-join: 4.0.1 + transitivePeerDependencies: + - debug + - supports-color + + husky@9.1.7: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.5.0 + + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.1.1 + yaml: 2.8.3 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + math-intrinsics@1.1.0: {} + + mime@1.6.0: {} + + mimic-function@5.0.1: {} + + minimist@1.2.8: {} + + ms@2.1.3: {} + + object-inspect@1.13.4: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + opener@1.5.2: {} + + picomatch@4.0.4: {} + + portfinder@1.0.38: + dependencies: + async: 3.2.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + requires-port@1.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + + safe-buffer@5.1.2: {} + + safer-buffer@2.1.2: {} + + secure-compare@3.0.1: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyexec@1.1.1: {} + + union@0.5.0: + dependencies: + qs: 6.15.1 + + url-join@4.0.1: {} + + whatwg-encoding@2.0.0: + dependencies: + iconv-lite: 0.6.3 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + yaml@2.8.3: {} diff --git a/pom.xml b/pom.xml index f0d4ef8..75e1313 100644 --- a/pom.xml +++ b/pom.xml @@ -12,12 +12,20 @@ SemossTemplate SemossTemplate ${ci.version} + 0.0.1-SNAPSHOT + java/src + + test + classes + + test-classes + java @@ -26,6 +34,13 @@ + + + + test/resources + + + maven-compiler-plugin @@ -37,7 +52,9 @@ + + central Maven Repository Switchboard @@ -47,6 +64,7 @@ false + 3rdPartyJARs Maven repository @@ -58,13 +76,72 @@ true + + + public + Sonatype's Maven repository + https://oss.sonatype.org/content/groups/public + + true + + + false + + + + + sonatype-snapshots + Sonatype Snapshots + https://central.sonatype.com/repository/maven-snapshots + + true + always + + + org.semoss semoss ${ci.version} provided + + + + org.junit.jupiter + junit-jupiter + 6.0.0 + test + + + + org.junit.platform + junit-platform-suite + 6.0.0 + test + + + + org.mockito + mockito-core + 5.18.0 + test + + + net.bytebuddy + byte-buddy + + + org.objenesis + objenesis + + + \ No newline at end of file diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..3ac8715 --- /dev/null +++ b/test/README.md @@ -0,0 +1,349 @@ +# Reactor Test Suite + +Comprehensive test suite for SEMOSS Template project reactors with organized test structure, helper utilities, and extensive test coverage. + +## Test Structure + +``` +test/ +├── reactors/ +│ ├── BaseReactorTest.java # Base test class with common mocking utilities +│ ├── GetWeatherReactorTest.java # Tests for GetWeatherReactor +│ └── ReactorTestSuite.java # Test suite to run all tests together +└── resources/ + └── log4j2-test.xml # Logging configuration for tests +``` + +## Running Tests + +### Run All Tests in the Suite + +```bash +mvn test -Dtest=ReactorTestSuite +``` + +### Run Individual Test Classes + +```bash +# Run GetWeatherReactor tests +mvn test -Dtest=GetWeatherReactorTest +``` + +### Run All Tests + +```bash +mvn test +``` + +### Run Specific Test Method + +```bash +mvn test -Dtest=GetWeatherReactorTest#testGetWeatherReactor_CustomCity +``` + +## Testing Workflow + +### Development Cycle + +Testing should be an integral part of your reactor development process: + +1. **Design Phase** - Plan your reactor's functionality and identify test scenarios +2. **Implementation** - Write your reactor code in `java/src/reactors/` +3. **Test Creation** - Create corresponding test class in `test/reactors/` +4. **Validation** - Run tests to verify behavior +5. **Iteration** - Fix issues and re-run tests until all pass +6. **Integration** - Add test to suite and commit + +### Recommended Testing Workflow + +#### Option 1: Test-Driven Development (TDD) +Write tests before implementing the reactor: + +```bash +# 1. Create test class first +# test/reactors/example/YourReactorTest.java + +# 2. Run tests (they will fail) +mvn test -Dtest=YourReactorTest + +# 3. Implement reactor to make tests pass +# java/src/reactors/examples/YourReactor.java + +# 4. Run tests again +mvn test -Dtest=YourReactorTest + +# 5. Refactor and repeat until all tests pass +``` + +#### Option 2: Traditional Development +Write reactor first, then add tests: + +```bash +# 1. Implement reactor +# java/src/reactors/examples/YourReactor.java + +# 2. Create comprehensive tests +# test/reactors/example/YourReactorTest.java + +# 3. Run tests to verify +mvn test -Dtest=YourReactorTest + +# 4. Fix any issues discovered +``` + +### Pre-Commit Testing + +Always run tests before committing changes: + +```bash +# Run all tests +mvn test + +# Or run just the tests for modified reactors +mvn test -Dtest=YourModifiedReactorTest + +# Stage and commit only after tests pass +git add . +git commit -m "feat: Add YourReactor with comprehensive tests" +``` + +### Continuous Testing During Development + +For rapid feedback during active development: + +```bash +# Terminal 1: Keep this running +mvn test -Dtest=YourReactorTest + +# Terminal 2: Edit your code +# Make changes to reactor or test + +# Return to Terminal 1 and re-run after each change +``` + +### Integration with SEMOSS Development + +When working with the SEMOSS UI: + +1. **Before "Recompile reactors"** in SEMOSS UI: + ```bash + mvn test # Ensure tests pass + ``` + +2. **After compiling** in SEMOSS UI: + - Test reactor in the application + - If issues found, update tests to cover the bug + - Fix reactor code + - Re-run tests + +3. **Before "Publish files"**: + ```bash + mvn test # Final verification + ``` + +### Multi-Reactor Development + +When working on multiple reactors: + +```bash +# Run tests for specific package +mvn test -Dtest=reactors.example.*Test + +# Or run the full suite +mvn test -Dtest=ReactorTestSuite +``` + +### Debugging Failed Tests + +1. **Read the error message** - JUnit provides detailed failure information +2. **Check mock setup** - Verify mocks are configured correctly +3. **Add debug logging** - Use `System.out.println()` in tests temporarily +4. **Run in debug mode** - Use your IDE's debugger to step through +5. **Isolate the issue** - Run single test method to focus + +```bash +# Run single test method with verbose output +mvn test -Dtest=YourReactorTest#testSpecificScenario -X +``` + +### Workflow Best Practices + +**Do:** +- Run tests frequently during development +- Write tests for bug fixes before fixing the bug +- Keep tests fast and focused +- Run full test suite before pushing to remote +- Update test documentation when adding new tests + +**Don't:** +- Skip writing tests for "simple" reactors +- Commit code with failing tests +- Ignore test failures in CI/CD +- Write overly complex tests that are hard to maintain +- Test implementation details instead of behavior + +## Test Coverage + +### GetWeatherReactor Tests +- Default weather retrieval +- Custom city parameter +- Error handling for missing parameters + +## BaseReactorTest Utilities + +The `BaseReactorTest` class provides common mocking utilities for all reactor tests: + +### Provided Mocks +- `@Mock Insight insight` - Mock insight for execution context +- `@Mock User user` - Mock user for authentication +- `@Mock NounStore nounStore` - Mock parameter storage +- `@Mock PyTranslator pyTranslator` - Mock Python integration +- `MockedStatic assetUtilsMock` - Mock asset utilities +- `Path tempDir` - Temporary directory for test files + +### Helper Methods + +#### Setting Reactor Parameters +```java +// Set string parameter +setReactorParameter(reactor, ReactorKeysEnum.NAME.getKey(), "Alice"); + +// Set numeric parameter +setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), 42); +``` + +#### Python Integration Setup +```java +// Mock Python module loading and function execution +setupPyTranslatorMocks("moduleName", "functionName", returnValue); +``` + +#### Creating Test Files +```java +// Create a Python file in the temp directory +createPythonFile("script.py", pythonCode); +``` + +#### Custom Project Properties +```java +@Override +protected void configureProjectProperties(Properties props) { + props.put("custom.property", "value"); +} +``` + +## Writing New Tests + +### Basic Test Structure + +```java +package reactors.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactors.BaseReactorTest; +import reactors.examples.YourReactor; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("YourReactor Tests") +public class YourReactorTest extends BaseReactorTest { + + private YourReactor reactor; + + @BeforeEach + void setup() { + reactor = new YourReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + } + + @Test + @DisplayName("Description of what this test does") + public void testYourReactor_Scenario() { + // Arrange: Set up parameters + setReactorParameter(reactor, "paramKey", "paramValue"); + + // Act: Execute reactor + NounMetadata result = reactor.execute(); + + // Assert: Verify results + assertNotNull(result); + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + } +} +``` + +### Add to Test Suite + +Update `ReactorTestSuite.java` to include your new test class: + +```java +@SelectClasses({ + HelloReactorTest.class, + CallPythonReactorTest.class, + OpenMCPAppReactorTest.class, + YourNewReactorTest.class // Add here +}) +``` + +## Test Dependencies + +All required dependencies are already configured in `pom.xml`: + +- **JUnit Jupiter 6.0.0** - Testing framework +- **JUnit Platform Suite 6.0.0** - Test suite support +- **Mockito 5.18.0** - Mocking framework + +## Best Practices + +1. **Extend BaseReactorTest** - Always extend `BaseReactorTest` for new reactor tests +2. **Use @DisplayName** - Add descriptive display names to tests and test classes +3. **Arrange-Act-Assert** - Follow AAA pattern in test methods +4. **Test Multiple Scenarios** - Test happy path, edge cases, and error conditions +5. **Use Helper Methods** - Leverage `BaseReactorTest` helper methods for cleaner tests +6. **Mock External Dependencies** - Use provided mocks for PyTranslator, AssetUtility, etc. +7. **Verify Interactions** - Use Mockito's `verify()` to ensure proper method calls + +## Continuous Integration + +These tests are designed to run in CI/CD pipelines. Ensure your CI configuration includes: + +```yaml +# Example for GitHub Actions +- name: Run Tests + run: mvn test +``` + +## Troubleshooting + +### Tests Failing Due to Missing Dependencies +```bash +mvn clean install +``` + +### Cannot Find Test Classes +Ensure the test source directory is correctly configured in `pom.xml`: +```xml +test +``` + +### Mock Setup Issues +Verify that `MockitoAnnotations.openMocks(this)` is called in `BaseReactorTest.baseSetup()` + +### Python Integration Tests Failing +Ensure `setupPyTranslatorMocks()` is called with correct module and function names + +## Additional Resources + +- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) +- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html) +- [SEMOSS Documentation](https://semoss.org/docs) + +## Contributing + +When adding new reactors, please: +1. Create corresponding test classes extending `BaseReactorTest` +2. Add comprehensive test coverage (minimum 3-5 test cases) +3. Update `ReactorTestSuite.java` to include new tests +4. Update this README with test coverage details diff --git a/test/reactors/BaseReactorTest.java b/test/reactors/BaseReactorTest.java new file mode 100644 index 0000000..b424d0e --- /dev/null +++ b/test/reactors/BaseReactorTest.java @@ -0,0 +1,219 @@ +package reactors; + +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import prerna.auth.AccessToken; +import prerna.auth.AuthProvider; +import prerna.auth.User; +import prerna.ds.py.PyTranslator; +import prerna.om.Insight; +import prerna.sablecc2.om.NounStore; +import prerna.util.AssetUtility; + +// Base test class providing common mocking utilities and setup for reactor +// tests. +// All reactor test classes should extend this class to leverage shared test +// infrastructure. +// +// This class handles: +// - Mock setup and teardown for common SEMOSS components +// - Temporary directory management for project assets +// - User authentication mocking +// - Project property file creation +// - PyTranslator mocking for Python-based reactors +public abstract class BaseReactorTest { + + // Mock insight providing context for reactor execution + @Mock + protected Insight insight; + + // Mock user for authentication context + @Mock + protected User user; + + // Mock noun store for reactor parameter management + @Mock + protected NounStore nounStore; + + // Mock PyTranslator for Python integration testing + @Mock + protected PyTranslator pyTranslator; + + // Static mock for AssetUtility to control project asset paths + protected MockedStatic assetUtilsMock; + + // AutoCloseable for managing Mockito annotations lifecycle + private AutoCloseable mocks; + + // Temporary directory for test execution + protected Path tempDir; + + // Fake project ID for testing + protected static final String TEST_PROJECT_ID = "test-project-id"; + + // Default test user name + protected static final String TEST_USER_NAME = "TestUser"; + + // Sets up the test environment before each test execution. + // Initializes mocks, creates temporary directories, and sets up + // common mock behaviors for insight, user, and asset utilities. + @BeforeEach + void baseSetup(@TempDir Path p) throws IOException { + tempDir = p; + mocks = MockitoAnnotations.openMocks(this); + + setupInsightMocks(); + setupUserMocks(); + setupAssetUtilityMocks(); + setupProjectProperties(); + } + + // Sets up mock behaviors for the Insight object. + // Configures project ID retrieval and user context. + protected void setupInsightMocks() { + when(insight.getUser()).thenReturn(user); + when(insight.getContextProjectId()).thenReturn(TEST_PROJECT_ID); + when(insight.getProjectId()).thenReturn(TEST_PROJECT_ID); + when(insight.getPyTranslator()).thenReturn(pyTranslator); + } + + // Sets up mock behaviors for the User object. + // Creates a mock access token with test user credentials. + protected void setupUserMocks() { + AccessToken token = new AccessToken(); + token.setName(TEST_USER_NAME); + token.setProvider(AuthProvider.NATIVE); + when(user.getPrimaryLoginToken()).thenReturn(token); + } + + // Sets up static mocking for AssetUtility. + // Configures the asset utility to return the temporary directory as the project + // assets folder. + protected void setupAssetUtilityMocks() { + assetUtilsMock = Mockito.mockStatic(AssetUtility.class); + assetUtilsMock.when(() -> AssetUtility.getProjectAssetsFolder(TEST_PROJECT_ID)) + .thenReturn(tempDir.toAbsolutePath().toString()); + } + + // Creates a project.properties file in the temporary directory. + // Subclasses can override configureProjectProperties(Properties) to + // add custom properties. + protected void setupProjectProperties() throws IOException { + Properties props = new Properties(); + configureProjectProperties(props); + + Path javaDir = tempDir.resolve("java"); + Files.createDirectories(javaDir); + Path projectPropertiesFile = javaDir.resolve("project.properties"); + + try (OutputStream os = Files.newOutputStream(projectPropertiesFile, + StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { + props.store(os, "Test project properties"); + } + } + + // Hook method for subclasses to configure custom project properties. + // Override this method to add specific properties needed for your reactor + // tests. + protected void configureProjectProperties(Properties props) { + // Default implementation - subclasses can override to add custom properties + } + + // Sets up mock behaviors for PyTranslator. + // Configures default responses for Python module loading and function + // execution. + protected void setupPyTranslatorMocks(String moduleName, String functionName, Object returnValue) { + when(pyTranslator.loadPythonModuleFromFile( + Mockito.eq(insight), + Mockito.anyString(), + Mockito.eq(TEST_PROJECT_ID))) + .thenReturn(moduleName); + + when(pyTranslator.runFunctionFromLoadedModule( + Mockito.eq(insight), + Mockito.eq(moduleName), + Mockito.eq(functionName), + Mockito.anyList())) + .thenReturn(returnValue); + } + + // Creates a Python source file in the temporary directory for testing. + // Useful for reactors that need to load Python files. + protected void createPythonFile(String fileName, String content) throws IOException { + Path pyDir = tempDir.resolve("py"); + Files.createDirectories(pyDir); + Path pythonFile = pyDir.resolve(fileName); + Files.writeString(pythonFile, content); + } + + // Helper method to set a parameter value on a reactor. + // This directly sets the value in the reactor's keyValue map via reflection. + protected void setReactorParameter(reactors.AbstractProjectReactor reactor, String key, String value) { + try { + // Access the protected keyValue map field via reflection + java.lang.reflect.Field keyValueField = findField(reactor.getClass(), "keyValue"); + keyValueField.setAccessible(true); + + @SuppressWarnings("unchecked") + java.util.Map keyValueMap = (java.util.Map) keyValueField.get(reactor); + + // Initialize the map if it's null + if (keyValueMap == null) { + keyValueMap = new java.util.HashMap<>(); + keyValueField.set(reactor, keyValueMap); + } + + // Add the parameter to the map + keyValueMap.put(key, value); + + } catch (Exception e) { + throw new RuntimeException("Failed to set reactor parameter: " + key, e); + } + } + + // Helper method to find a field in the class hierarchy. + private java.lang.reflect.Field findField(Class clazz, String fieldName) throws NoSuchFieldException { + Class current = clazz; + while (current != null) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + // Helper method to set a numeric parameter on a reactor. + // Converts the number to a string automatically. + protected void setReactorParameter(reactors.AbstractProjectReactor reactor, String key, int value) { + setReactorParameter(reactor, key, String.valueOf(value)); + } + + // Cleans up test resources after each test execution. + // Closes static mocks and Mockito annotations. + @AfterEach + void baseTearDown() throws Exception { + if (assetUtilsMock != null) { + assetUtilsMock.close(); + } + if (mocks != null) { + mocks.close(); + } + } +} diff --git a/test/reactors/GetWeatherReactorTest.java b/test/reactors/GetWeatherReactorTest.java new file mode 100644 index 0000000..1fe2dfa --- /dev/null +++ b/test/reactors/GetWeatherReactorTest.java @@ -0,0 +1,113 @@ +package reactors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.PixelOperationType; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +// Tests for GetWeatherReactor. +// +// Verifies the reactor's output for valid cities, missing parameters, +// cities with spaces, and reactor/key description methods. +@DisplayName("GetWeatherReactor Tests") +public class GetWeatherReactorTest extends BaseReactorTest { + + private GetWeatherReactor reactor; + + // Initializes a fresh GetWeatherReactor before each test. + @BeforeEach + void setup() { + reactor = new GetWeatherReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + } + + // Verifies that a valid city returns the expected sunny forecast string. + @Test + @DisplayName("Should return sunny forecast for a given city") + void testGetWeather_validCity() { + setReactorParameter(reactor, "city", "Boston"); + + NounMetadata result = reactor.execute(); + + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + String forecast = (String) result.getValue(); + assertNotNull(forecast); + assertEquals("It will be sunny in Boston today.", forecast); + } + + // Verifies that the city name is included in the response for any city. + @Test + @DisplayName("Should include city name in response for different cities") + void testGetWeather_differentCity() { + setReactorParameter(reactor, "city", "Tokyo"); + + NounMetadata result = reactor.execute(); + + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + String forecast = (String) result.getValue(); + assertTrue(forecast.contains("Tokyo")); + } + + // Verifies that omitting the city parameter results in an error response. + // AbstractProjectReactor wraps unhandled exceptions as error responses. + @Test + @DisplayName("Should return error when city parameter is missing") + void testGetWeather_missingCity() { + // Don't set the city parameter — let it be null + NounMetadata result = reactor.execute(); + + // AbstractProjectReactor wraps exceptions as error responses + assertTrue( + result.getOpType().contains(PixelOperationType.ERROR) + || result.getValue().toString().toLowerCase().contains("null"), + "Expected an error or null-related message when city is missing" + ); + } + + // Verifies that city names containing spaces are handled correctly. + @Test + @DisplayName("Should handle city with spaces") + void testGetWeather_cityWithSpaces() { + setReactorParameter(reactor, "city", "New York"); + + NounMetadata result = reactor.execute(); + + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + String forecast = (String) result.getValue(); + assertEquals("It will be sunny in New York today.", forecast); + } + + // Verifies that getReactorDescription() returns a non-null weather-related string. + @Test + @DisplayName("Should return correct reactor description") + void testGetReactorDescription() { + String desc = reactor.getReactorDescription(); + assertNotNull(desc); + assertTrue(desc.toLowerCase().contains("weather")); + } + + // Verifies that getDescriptionForKey("city") returns a city-related description. + @Test + @DisplayName("Should return correct description for city key") + void testGetDescriptionForKey_city() { + String desc = reactor.getDescriptionForKey("city"); + assertNotNull(desc); + assertTrue(desc.toLowerCase().contains("city")); + } + + // Verifies that getDescriptionForKey() returns null for unknown keys. + @Test + @DisplayName("Should return null description for unknown key") + void testGetDescriptionForKey_unknown() { + String desc = reactor.getDescriptionForKey("nonexistent"); + assertEquals(null, desc); + } +} diff --git a/test/reactors/ReactorTestSuite.java b/test/reactors/ReactorTestSuite.java new file mode 100644 index 0000000..a881a9a --- /dev/null +++ b/test/reactors/ReactorTestSuite.java @@ -0,0 +1,18 @@ +package reactors; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +// Test suite that runs all reactor tests in the project. +// +// mvn test -Dtest=ReactorTestSuite +@Suite +@SuiteDisplayName("Reactor Test Suite") +@SelectClasses({ + GetWeatherReactorTest.class +}) +public class ReactorTestSuite { + // This class remains empty, it is used only as a holder for the above + // annotations +} diff --git a/test/resources/log4j2-test.xml b/test/resources/log4j2-test.xml new file mode 100644 index 0000000..d928d50 --- /dev/null +++ b/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file