Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,88 @@ jobs:
exit 1
fi

- name: Run per-package tests
if: success()
run: |
PORT_VAR="PORT_${{ matrix.cfengine }}"
PORT="${!PORT_VAR}"

PKG_DB="sqlite"

# Find packages with test directories
if [ ! -d "packages" ]; then
echo "No packages/ directory found, skipping"
exit 0
fi

PKG_FAIL=0
for pkg_dir in packages/*/; do
pkg_name=$(basename "$pkg_dir")
test_dir="${pkg_dir}tests"

if [ ! -d "$test_dir" ]; then
echo "Package '${pkg_name}' has no tests/ directory, skipping"
continue
fi

echo ""
echo "=============================================="
echo "Testing package: ${pkg_name} (${{ matrix.cfengine }} + ${PKG_DB})"
echo "=============================================="

# Activate: copy package to vendor/
cp -r "${pkg_dir}" "vendor/${pkg_name}"

# Restart engine for clean app state with the package loaded
docker restart wheels-${{ matrix.cfengine }}-1
WAIT=0
while [ "$WAIT" -lt 30 ]; do
WAIT=$((WAIT + 1))
if curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" | grep -q "200\|404\|302"; then
break
fi
sleep 5
done
# Warm-up
curl -s -o /dev/null --max-time 60 "http://localhost:${PORT}/?reload=true" || true
sleep 2

# Run the package's tests
TEST_URL="http://localhost:${PORT}/wheels/core/tests?db=${PKG_DB}&format=json&directory=vendor.${pkg_name}.tests"
RESULT_FILE="/tmp/test-results/${{ matrix.cfengine }}-pkg-${pkg_name}.txt"

HTTP_CODE=$(curl -sL -o "$RESULT_FILE" \
--max-time 300 \
--write-out "%{http_code}" \
"$TEST_URL" || echo "000")

if [ "$HTTP_CODE" = "200" ]; then
PASS=$(python3 -c "import json; d=json.load(open('$RESULT_FILE')); print(d.get('totalPass',0))" 2>/dev/null || echo "?")
echo "PASSED: package '${pkg_name}' (${PASS} specs)"
else
echo "::warning::Package '${pkg_name}' tests returned HTTP ${HTTP_CODE}"
PKG_FAIL=1
fi

# Deactivate: remove from vendor/
rm -rf "vendor/${pkg_name}"
done

# Restart engine to restore clean state (no packages)
docker restart wheels-${{ matrix.cfengine }}-1
WAIT=0
while [ "$WAIT" -lt 30 ]; do
WAIT=$((WAIT + 1))
if curl -s -o /dev/null --connect-timeout 2 --max-time 5 -w "%{http_code}" "http://localhost:${PORT}/" | grep -q "200\|404\|302"; then
break
fi
sleep 5
done

if [ "$PKG_FAIL" -ne 0 ]; then
echo "::warning::One or more package test suites had issues (non-blocking)"
fi

- name: Generate per-engine summary
if: always()
run: |
Expand Down
57 changes: 56 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ app/migrator/migrations/ app/db/seeds.cfm app/db/seeds/
app/events/ app/global/ app/lib/
app/mailers/ app/jobs/ app/plugins/ app/snippets/
config/settings.cfm config/routes.cfm config/environment.cfm
public/ tests/ vendor/ .env (never commit)
packages/ plugins/ public/ tests/ vendor/ .env (never commit)
```

## Development Tools
Expand Down Expand Up @@ -331,6 +331,61 @@ new wheels.middleware.RateLimiter(keyFunction=function(req) {

Strategies: `fixedWindow` (default), `slidingWindow`, `tokenBucket`. Storage: `memory` (default) or `database`. Adds `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers. Returns `429 Too Many Requests` with `Retry-After` when limit exceeded.

## Package System

Optional first-party modules ship in `packages/` and are activated by copying to `vendor/`. The framework auto-discovers `vendor/*/package.json` on startup via `PackageLoader.cfc` with per-package error isolation.

```
packages/ # Source/staging (NOT auto-loaded)
sentry/ # wheels-sentry — error tracking
hotwire/ # wheels-hotwire — Turbo/Stimulus
basecoat/ # wheels-basecoat — UI components
vendor/ # Runtime: framework core + activated packages
wheels/ # Framework core (excluded from package discovery)
sentry/ # Activated package (copied from packages/)
plugins/ # DEPRECATED: legacy plugins still work with warning
```

### package.json Manifest

```json
{
"name": "wheels-sentry",
"version": "1.0.0",
"author": "PAI Industries",
"description": "Sentry error tracking",
"wheelsVersion": ">=3.0",
"provides": {
"mixins": "controller",
"services": [],
"middleware": []
},
"dependencies": {}
}
```

**`provides.mixins`**: Comma-delimited targets — `controller`, `view`, `model`, `global`, `none`. Determines which framework components receive the package's public methods. Default: `none` (explicit opt-in, unlike legacy plugins which default to `global`).

### Activating a Package

```bash
cp -r packages/sentry vendor/sentry # activate
rm -rf vendor/sentry # deactivate
```

Restart or reload the app after activation. Symlinks also work: `ln -s ../../packages/sentry vendor/sentry`.

### Error Isolation

Each package loads in its own try/catch. A broken package is logged and skipped — the app and other packages continue normally.

### Testing Packages

```bash
# Run a specific package's tests (package must be in vendor/)
curl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&directory=vendor.sentry.tests"
```

## Routing Quick Reference

```cfm
Expand Down
Loading
Loading