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