#!/usr/bin/env bash # # test-external-plugin.sh # # Verify an out-of-tree shopdb-flask plugin against a pinned framework build. # Stand up a throwaway copy of the framework, drop the plugin into # plugins// as a symlink (the mechanism ADR-003 documents for sister # sites), and run the framework contract tests plus the plugin's own tests. # Nonzero exit means the plugin is not compatible with that framework ref. # # Two modes: # # CI / remote (default): clone the framework at FRAMEWORK_REF from # FRAMEWORK_URL, build a fresh venv, pip install requirements. Needs # network access to git and PyPI. # # Local / offline: set LOCAL_FRAMEWORK to a framework checkout on disk. # The script exports that checkout at HEAD with `git archive` (no network) # and reuses the checkout's existing venv, so it runs with no internet. # # Inputs (env var, or positional): # PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required. # FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main. # FRAMEWORK_URL framework git URL for CI mode. # Default: https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git # LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline. # # Examples: # # CI: test the plugin in the current repo against a pinned tag # PLUGIN_DIR=. FRAMEWORK_REF=v0.5.0 scripts/test-external-plugin.sh # # # Offline: test a bundled plugin against this checkout, no network # LOCAL_FRAMEWORK=. PLUGIN_DIR=plugins/warranty scripts/test-external-plugin.sh # # Note on coverage: a symlinked plugin is discovered and loaded by the plugin # loader (so loadability, manifest validity, and the core_version range are all # checked), but the import-surface scan in test_plugin_contract.py uses # Path.rglob over plugins/, which does not descend symlinked directories on # CPython 3.12. To have that scan cover your plugin too, keep an equivalent # import-surface assertion in your own tests/ (see docs/PLUGIN-EXTERNAL-REPO.md). set -eu PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}" FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}" FRAMEWORK_URL="${FRAMEWORK_URL:-https://gitea.proudtech.net/ge-aerospace/shopdb-flask.git}" LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}" if [ -z "$PLUGIN_DIR" ]; then echo "ERROR: PLUGIN_DIR is required (env var or first argument)." >&2 exit 2 fi if [ ! -f "$PLUGIN_DIR/manifest.json" ]; then echo "ERROR: $PLUGIN_DIR has no manifest.json - not a plugin directory." >&2 exit 2 fi # Absolute path so the symlink still resolves after we cd into the temp tree. PLUGIN_ABS="$(cd "$PLUGIN_DIR" && pwd)" # Plugin name: prefer the manifest name, fall back to the directory basename. PLUGIN_NAME="$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_ABS/manifest.json" | head -n1)" if [ -z "$PLUGIN_NAME" ]; then PLUGIN_NAME="$(basename "$PLUGIN_ABS")" fi WORKDIR="$(mktemp -d)" cleanup() { rm -rf "$WORKDIR"; } trap cleanup EXIT FRAMEWORK="$WORKDIR/framework" mkdir -p "$FRAMEWORK" if [ -n "$LOCAL_FRAMEWORK" ]; then echo "==> Local mode: exporting framework from $LOCAL_FRAMEWORK (HEAD)" LOCAL_ABS="$(cd "$LOCAL_FRAMEWORK" && pwd)" git -C "$LOCAL_ABS" archive HEAD | tar -x -C "$FRAMEWORK" # Reuse the checkout's venv so no pip and no network are needed. if [ -x "$LOCAL_ABS/venv/bin/python" ]; then PYTHON="$LOCAL_ABS/venv/bin/python" elif [ -x "$LOCAL_ABS/.venv/bin/python" ]; then PYTHON="$LOCAL_ABS/.venv/bin/python" else echo "ERROR: no venv found under $LOCAL_ABS (looked for venv/ and .venv/)." >&2 exit 2 fi else echo "==> CI mode: cloning $FRAMEWORK_URL @ $FRAMEWORK_REF" git clone --depth 1 --branch "$FRAMEWORK_REF" "$FRAMEWORK_URL" "$FRAMEWORK" echo "==> Creating venv and installing requirements" python3 -m venv "$WORKDIR/venv" PYTHON="$WORKDIR/venv/bin/python" "$PYTHON" -m pip install --upgrade pip >/dev/null "$PYTHON" -m pip install -r "$FRAMEWORK/requirements.txt" fi echo "==> Linking plugin '$PLUGIN_NAME' into framework plugins/" rm -rf "$FRAMEWORK/plugins/$PLUGIN_NAME" ln -s "$PLUGIN_ABS" "$FRAMEWORK/plugins/$PLUGIN_NAME" # Drop any exported registry so the loader re-discovers and enables the plugin # (the test conftest seeds a fresh registry from plugins/*/manifest.json). rm -f "$FRAMEWORK/instance/plugins.json" RC=0 echo "==> Running framework contract tests" ( cd "$FRAMEWORK" && "$PYTHON" -m pytest tests/test_plugin_contract.py -q ) || RC=1 if [ -d "$PLUGIN_ABS/tests" ]; then echo "==> Running plugin's own tests" ( cd "$FRAMEWORK" && "$PYTHON" -m pytest "$PLUGIN_ABS/tests" -q ) || RC=1 else echo "==> Plugin has no tests/ directory - skipping plugin test step" fi if [ "$RC" -eq 0 ]; then echo "==> PASS: plugin '$PLUGIN_NAME' is compatible with framework ref '$FRAMEWORK_REF'" else echo "==> FAIL: plugin '$PLUGIN_NAME' - see output above" >&2 fi exit "$RC"