From 27a8e987a6f6fe05fc09f3965e7bdff00dd31204 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 08:56:40 +0000 Subject: [PATCH 1/3] Make the cloud SessionStart hook async and fix MariaDB test auth Cloud sessions sat at their first turn for ~10 minutes because the synchronous SessionStart hook ran a cold `composer install`. The session's GitHub proxy refuses archive downloads (api.github.com zipball, codeload) with 403 for repos not attached to the session, so all 43 Composer packages fell back to sequential git clones. - Split the hook: session-env.sh (sync, instant) writes the test DB env vars and PATH; session-start.sh runs async and logs to /tmp/rfsd-session-setup.log, writing /tmp/rfsd-session-setup.done on success. - Skip composer/npm when composer.lock/package-lock.json are unchanged. - Use --prefer-source since dist downloads from GitHub always 403 here. - Each step is logged with its duration; one failure no longer stops the rest. - Fix the MariaDB password check: over the Unix socket, unix_socket auth lets OS root in with any password, so the password was never set and PHPUnit (TCP 127.0.0.1) got "Access denied". Check over TCP instead. - Document the background setup in CLAUDE.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016QZq4ts3V2N5nh2sZeUHm2 --- .claude/hooks/session-env.sh | 19 +++++ .claude/hooks/session-start.sh | 123 +++++++++++++++++++++++---------- .claude/settings.json | 7 +- CLAUDE.md | 2 + 4 files changed, 115 insertions(+), 36 deletions(-) create mode 100755 .claude/hooks/session-env.sh 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..b55470f 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 +# Tell Claude Code to run the rest of this hook in the background (30 min cap). +echo '{"async": true, "asyncTimeout": 1800000}' + cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" -# Database settings read by tests/php/wp-tests-config.php. +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..6477374 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,8 @@ composer build-zip && composer check-zip # release zip from `git ar composer bump-patch # or bump-minor / bump-major ``` +In Claude Code cloud sessions, `.claude/hooks/session-start.sh` installs Composer/npm dependencies and MariaDB **in the background** (a cold Composer install takes ~10 minutes). Before running `composer lint`/`composer test`/`npm test`, check that `/tmp/rfsd-session-setup.done` exists; if not, `tail /tmp/rfsd-session-setup.log` and wait for it, rather than running `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 From 3d4690bfcc146de6466461a02bd5d861d0c4e422 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 08:58:30 +0000 Subject: [PATCH 2/3] Exit the session hook if cd into the project fails (SC2164) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016QZq4ts3V2N5nh2sZeUHm2 --- .claude/hooks/session-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index b55470f..5a432cd 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -16,7 +16,7 @@ fi # Tell Claude Code to run the rest of this hook in the background (30 min cap). echo '{"async": true, "asyncTimeout": 1800000}' -cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" +cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" || exit 1 LOG=/tmp/rfsd-session-setup.log DONE=/tmp/rfsd-session-setup.done From ebf17a8ed4ae691c93c6ed381e1c87a97f17ebb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 09:11:16 +0000 Subject: [PATCH 3/3] Split the cloud-session note in CLAUDE.md into short bullets Codacy (Agentlinter) flagged the 45-word sentence as overly complex. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016QZq4ts3V2N5nh2sZeUHm2 --- CLAUDE.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6477374..b1d8e95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,11 @@ composer build-zip && composer check-zip # release zip from `git ar composer bump-patch # or bump-minor / bump-major ``` -In Claude Code cloud sessions, `.claude/hooks/session-start.sh` installs Composer/npm dependencies and MariaDB **in the background** (a cold Composer install takes ~10 minutes). Before running `composer lint`/`composer test`/`npm test`, check that `/tmp/rfsd-session-setup.done` exists; if not, `tail /tmp/rfsd-session-setup.log` and wait for it, rather than running `composer install` yourself. +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`.