diff --git a/.claude/hooks/session-env.sh b/.claude/hooks/session-env.sh new file mode 100755 index 0000000..4ca23d0 --- /dev/null +++ b/.claude/hooks/session-env.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Synchronous and instant: only writes static session environment variables. +# The slow dependency installs live in session-start.sh, which runs async. +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ] || [ -z "${CLAUDE_ENV_FILE:-}" ]; then + exit 0 +fi + +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" + +# Database settings read by tests/php/wp-tests-config.php (must match session-start.sh). +{ + echo 'export WP_TESTS_DB_HOST="127.0.0.1:3306"' + echo 'export WP_TESTS_DB_USER="root"' + echo 'export WP_TESTS_DB_PASSWORD="root"' + echo 'export WP_TESTS_DB_NAME="wordpress_test"' + echo "export PATH=\"$PWD/vendor/bin:\$PATH\"" +} >> "$CLAUDE_ENV_FILE" diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index ce0a2d8..5a432cd 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,59 +1,112 @@ #!/bin/bash # Prepares a Claude Code cloud session so linters and tests can run: # Composer + npm dependencies, plus a local MariaDB for the WordPress PHPUnit suite. -set -euo pipefail +# +# Runs ASYNC: the session's first turn starts immediately while this installs in +# the background. A cold run takes ~10 minutes, almost all of it Composer: the +# session's GitHub proxy refuses archive (zipball/codeload) downloads for repos +# that aren't attached to the session, so every package is git-cloned instead. +# Progress goes to $LOG; $DONE is written when everything finished successfully. +set -uo pipefail if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi -cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" +# Tell Claude Code to run the rest of this hook in the background (30 min cap). +echo '{"async": true, "asyncTimeout": 1800000}' -# Database settings read by tests/php/wp-tests-config.php. +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" || exit 1 + +LOG=/tmp/rfsd-session-setup.log +DONE=/tmp/rfsd-session-setup.done +rm -f "$DONE" +exec >>"$LOG" 2>&1 +echo "=== session setup started $(date -u +%FT%TZ)" + +# Database settings read by tests/php/wp-tests-config.php (must match session-env.sh). DB_NAME="wordpress_test" DB_USER="root" DB_PASS="root" -# --- PHP dependencies (phpcs, phpunit, WordPress test suite) --- -# Cloud sessions run as root; without this Composer disables plugins, so the -# phpcs installer plugin never registers the WordPress coding standard. -export COMPOSER_ALLOW_SUPERUSER=1 -composer install --no-interaction --no-progress --prefer-dist +failed=() -# --- JS dependencies (jest) --- -npm install --no-audit --no-fund +# Runs a step, logging its duration; a failed step is recorded and the rest continue. +step() { + local name=$1 + shift + local start=$SECONDS + echo "--- $name" + if "$@"; then + echo "--- $name ok ($((SECONDS - start))s)" + else + echo "--- $name FAILED ($((SECONDS - start))s)" + failed+=("$name") + fi +} -# --- MariaDB for the PHPUnit integration tests --- -if ! command -v mariadbd >/dev/null 2>&1 && ! command -v mysqld >/dev/null 2>&1; then - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq - apt-get install -y -qq mariadb-server -fi +# Skips an install when the files that decide it are unchanged since it last succeeded. +# The hash file lives in the git-ignored directory the install fills. +up_to_date() { + sha256sum --check --status "$1" 2>/dev/null +} -if ! mysqladmin ping --silent >/dev/null 2>&1; then - service mariadb start 2>/dev/null || service mysql start - for _ in $(seq 1 30); do - mysqladmin ping --silent >/dev/null 2>&1 && break - sleep 1 - done -fi +composer_install() { + local hashes=vendor/.session-start.sha256 + up_to_date "$hashes" && { echo "vendor/ up to date"; return 0; } + rm -f "$hashes" + # Cloud sessions run as root; without this Composer disables plugins, so the + # phpcs installer plugin never registers the WordPress coding standard. + # --prefer-source: dist archives from GitHub are always refused by the session + # proxy (403), so skip straight to git clones instead of failing 43 downloads. + COMPOSER_ALLOW_SUPERUSER=1 composer install --no-interaction --no-progress --prefer-source && + sha256sum composer.json composer.lock .claude/hooks/session-start.sh > "$hashes" +} + +npm_install() { + local hashes=node_modules/.session-start.sha256 + up_to_date "$hashes" && { echo "node_modules/ up to date"; return 0; } + rm -f "$hashes" + npm install --no-audit --no-fund && + sha256sum package.json package-lock.json > "$hashes" +} -# Idempotent: password-authenticated root over TCP and a fresh test database. -if ! mysql -u"$DB_USER" -p"$DB_PASS" -e "CREATE DATABASE IF NOT EXISTS $DB_NAME" >/dev/null 2>&1; then +mariadb_setup() { + if ! command -v mariadbd >/dev/null 2>&1 && ! command -v mysqld >/dev/null 2>&1; then + DEBIAN_FRONTEND=noninteractive apt-get update -qq && + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq mariadb-server || return 1 + fi + + if ! mysqladmin ping --silent >/dev/null 2>&1; then + service mariadb start 2>/dev/null || service mysql start || return 1 + for _ in $(seq 1 30); do + mysqladmin ping --silent >/dev/null 2>&1 && break + sleep 1 + done + fi + + # Idempotent: password-authenticated root over TCP and a fresh test database. + # The check must use TCP (-h 127.0.0.1) like PHPUnit does: over the Unix socket, + # MariaDB's unix_socket auth lets OS root in with any password, so the check + # would pass without the password ever being set. + mysql -h127.0.0.1 -P3306 -u"$DB_USER" -p"$DB_PASS" -e "CREATE DATABASE IF NOT EXISTS $DB_NAME" >/dev/null 2>&1 && return 0 mysql -uroot <> "$CLAUDE_ENV_FILE" +if [ ${#failed[@]} -eq 0 ]; then + date -u +%FT%TZ > "$DONE" + echo "=== session setup finished OK $(date -u +%FT%TZ)" +else + echo "=== session setup finished with failures: ${failed[*]} $(date -u +%FT%TZ)" + exit 1 fi diff --git a/.claude/settings.json b/.claude/settings.json index d9f7180..402ea9b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,10 +4,15 @@ { "matcher": "startup|resume", "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-env.sh", + "timeout": 30 + }, { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh", - "timeout": 1800 + "timeout": 60 } ] } diff --git a/CLAUDE.md b/CLAUDE.md index b03b28e..b1d8e95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,12 @@ composer build-zip && composer check-zip # release zip from `git ar composer bump-patch # or bump-minor / bump-major ``` +Cloud sessions install dependencies in the background: + +- `.claude/hooks/session-start.sh` installs Composer, npm and MariaDB. A cold run takes about 10 minutes. +- Before linting or testing, check that `/tmp/rfsd-session-setup.done` exists. +- If it doesn't, follow `/tmp/rfsd-session-setup.log` and wait. Don't run `composer install` yourself. + PHPUnit runs against a real MySQL/MariaDB database that the suite **wipes on every run**. Settings come from `.env.testing` (copy `.env.testing.example`); shell env vars win. Tests are loaded by `tests/php/bootstrap.php`, which loads the plugin on `muplugins_loaded` and calls `rfsd_activate()` on `init`. Extend `RFSD_TestCase` (`tests/php/class-rfsd-testcase.php`, has `create_faq()` helpers) or `RFSD_Multisite_TestCase`. Tag network-only tests `@group ms-required` and single-site-only tests `@group ms-excluded`; each config excludes the other group. PHPUnit converts notices, warnings and deprecations to failures. WP-CLI is stubbed in `tests/php/stubs/class-wp-cli.php`. ## Architecture