diff --git a/.claude/hooks/setup-cloud.sh b/.claude/hooks/setup-cloud.sh new file mode 100755 index 000000000..07be46627 --- /dev/null +++ b/.claude/hooks/setup-cloud.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# SessionStart hook for Claude Code web (cloud sandbox). +# Skipped entirely when running locally (CLI/IDE). +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +echo "Cloud sandbox detected — running setup…" + +# ── 1. apt over HTTPS (egress proxy workaround) ────────── +sudo sed -i \ + 's|http://archive.ubuntu.com|https://archive.ubuntu.com|g' \ + /etc/apt/sources.list.d/ubuntu.sources +sudo sed -i \ + 's|http://security.ubuntu.com|https://security.ubuntu.com|g' \ + /etc/apt/sources.list.d/ubuntu.sources +sudo apt-get update -qq + +# ── 2. System packages ─────────────────────────────────── +sudo apt-get install -y -qq \ + libpq-dev libcairo2-dev libpango1.0-dev \ + libgdk-pixbuf2.0-dev libffi-dev \ + libgirepository1.0-dev libgtk-3-dev \ + libldap2-dev libsasl2-dev python3-dev \ + postgresql-plpython3-16 + +# ── 3. Polish locale + PostgreSQL restart ───────────────── +sudo locale-gen pl_PL.UTF-8 +sudo pg_ctlcluster 16 main restart + +# ── 4. PostgreSQL trust auth ────────────────────────────── +PG_HBA=/etc/postgresql/16/main/pg_hba.conf +sudo sed -i \ + 's/^local.*all.*all.*peer/local all all trust/' \ + "$PG_HBA" +sudo sed -i \ + 's/^host.*all.*all.*127.0.0.1\/32.*scram-sha-256/host all all 127.0.0.1\/32 trust/' \ + "$PG_HBA" +sudo pg_ctlcluster 16 main reload + +# ── 5. Database ─────────────────────────────────────────── +sudo -u postgres psql -c \ + "SELECT 1 FROM pg_roles WHERE rolname='bpp'" \ + | grep -q 1 \ + || sudo -u postgres psql -c \ + "CREATE USER bpp WITH PASSWORD '' SUPERUSER;" +sudo -u postgres psql -tc \ + "SELECT 1 FROM pg_database WHERE datname='bpp'" \ + | grep -q 1 \ + || sudo -u postgres psql -c \ + "CREATE DATABASE bpp OWNER bpp;" + +# ── 6. Redis ────────────────────────────────────────────── +redis-cli ping >/dev/null 2>&1 \ + || redis-server --daemonize yes + +# ── 7. .env ─────────────────────────────────────────────── +if [ ! -f "$CLAUDE_PROJECT_DIR/.env" ]; then +cat > "$CLAUDE_PROJECT_DIR/.env" << 'DOTENV' +DJANGO_SETTINGS_MODULE="django_bpp.settings.local" +DEBUG=true +DJANGO_BPP_HOSTNAME="localhost" +DJANGO_BPP_SECRET_KEY="test-secret-key-for-local-dev" +DJANGO_BPP_DB_NAME="bpp" +DJANGO_BPP_DB_USER="bpp" +DJANGO_BPP_DB_PASSWORD="" +DJANGO_BPP_DB_HOST="localhost" +DJANGO_BPP_DB_PORT="5432" +DJANGO_BPP_DB_DISABLE_SSL=1 +DJANGO_BPP_REDIS_HOST="localhost" +DJANGO_BPP_REDIS_PORT="6379" +DJANGO_BPP_REDIS_DB_CELERY="2" +DJANGO_BPP_REDIS_DB_SESSION="4" +DJANGO_BPP_REDIS_DB_CACHE="5" +DJANGO_BPP_REDIS_DB_LOCKS="6" +DJANGO_BPP_RABBITMQ_HOST="localhost" +DJANGO_BPP_RABBITMQ_PORT="5672" +DJANGO_BPP_RABBITMQ_USER="bpp" +DJANGO_BPP_RABBITMQ_PASS="bpp" +DJANGO_BPP_UZYWAJ_PUNKTACJI_WEWNETRZNEJ="0" +DJANGO_BPP_PUNKTUJ_MONOGRAFIE="0" +DJANGO_BPP_INLINE_DLA_AUTOROW="stacked" +DJANGO_BPP_THEME_NAME="app-blue" +DJANGO_BPP_SESSION_SECURITY_WARN_AFTER="25200" +DJANGO_BPP_SESSION_SECURITY_EXPIRE_AFTER="28800" +DJANGO_BPP_PASSWORD_DURATION_SECONDS="315360000" +DJANGO_BPP_USE_PASSWORD_HISTORY="1" +DJANGO_BPP_PASSWORD_HISTORY_COUNT="1" +DOTENV +fi + +# ── 8. Python dependencies ──────────────────────────────── +cd "$CLAUDE_PROJECT_DIR" +uv sync --all-extras || true + +# psycopg2 workaround: install binary, fake dist-info +if ! python -c "import psycopg2" 2>/dev/null; then + uv pip install psycopg2-binary==2.9.11 + SITE_PKG=$(.venv/bin/python -c \ + "import site; print(site.getsitepackages()[0])") + mkdir -p "$SITE_PKG/psycopg2-2.9.11.dist-info" + cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/METADATA" \ + << 'META' +Metadata-Version: 2.1 +Name: psycopg2 +Version: 2.9.11 +META + cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/RECORD" \ + << 'REC' +psycopg2/__init__.py,sha256=, +psycopg2-2.9.11.dist-info/METADATA,sha256=, +psycopg2-2.9.11.dist-info/RECORD,, +REC + echo "uv" > \ + "$SITE_PKG/psycopg2-2.9.11.dist-info/INSTALLER" + cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/WHEEL" \ + << 'WHL' +Wheel-Version: 1.0 +Generator: manual +Root-Is-Purelib: false +Tag: cp311-cp311-linux_x86_64 +WHL +fi + +echo "Cloud setup complete." diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..f92640f73 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/setup-cloud.sh\"" + } + ] + } + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 6aa82f113..468959e2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,8 +43,11 @@ pytest src/app_name/tests/ ## Claude Code Web Environment Setup -For cloud sandbox setup instructions, see -[CLAUDE_CLOUD.md](CLAUDE_CLOUD.md). +Cloud sandbox setup runs automatically via a `SessionStart` hook +(`.claude/hooks/setup-cloud.sh`). The hook checks +`CLAUDE_CODE_REMOTE` and exits immediately on CLI — no manual +steps needed. For sandbox-specific notes (known test failures, +limitations), see [CLAUDE_CLOUD.md](CLAUDE_CLOUD.md). ## Key Commands (Quick Reference) diff --git a/CLAUDE_CLOUD.md b/CLAUDE_CLOUD.md index fd483221e..cd01705a5 100644 --- a/CLAUDE_CLOUD.md +++ b/CLAUDE_CLOUD.md @@ -1,149 +1,9 @@ -# Claude Code Web Environment Setup +# Claude Code Web Environment — Notes -When running in Claude Code web environment (cloud sandbox), -the following setup steps are needed before running tests. -These steps handle missing system packages, services, and -workarounds for network restrictions in the sandbox. +Cloud sandbox setup runs automatically via the `SessionStart` +hook in `.claude/hooks/setup-cloud.sh`. No manual steps needed. -## 1. Configure apt to use HTTPS (network proxy workaround) - -The sandbox routes traffic through an egress proxy that only -allows HTTPS to whitelisted domains. Apt must use HTTPS: - -```bash -# Switch apt sources to HTTPS -sudo sed -i \ - 's|http://archive.ubuntu.com|https://archive.ubuntu.com|g' \ - /etc/apt/sources.list.d/ubuntu.sources -sudo sed -i \ - 's|http://security.ubuntu.com|https://security.ubuntu.com|g' \ - /etc/apt/sources.list.d/ubuntu.sources -sudo apt-get update -``` - -## 2. Install system dependencies - -```bash -sudo apt-get install -y \ - libpq-dev libcairo2-dev libpango1.0-dev \ - libgdk-pixbuf2.0-dev libffi-dev \ - libgirepository1.0-dev libgtk-3-dev \ - libldap2-dev libsasl2-dev python3-dev \ - postgresql-plpython3-16 -``` - -## 3. Generate Polish locale and restart PostgreSQL - -```bash -sudo locale-gen pl_PL.UTF-8 -sudo pg_ctlcluster 16 main restart -``` - -## 4. Configure PostgreSQL for local trust auth - -```bash -sudo sed -i \ - 's/^local.*all.*all.*peer/local all all trust/' \ - /etc/postgresql/16/main/pg_hba.conf -sudo sed -i \ - 's/^host.*all.*all.*127.0.0.1\/32.*scram-sha-256/host all all 127.0.0.1\/32 trust/' \ - /etc/postgresql/16/main/pg_hba.conf -sudo pg_ctlcluster 16 main reload -``` - -## 5. Create database user and database - -```bash -sudo -u postgres psql -c \ - "CREATE USER bpp WITH PASSWORD '' SUPERUSER;" -sudo -u postgres psql -c \ - "CREATE DATABASE bpp OWNER bpp;" -``` - -## 6. Start Redis - -```bash -redis-server --daemonize yes -``` - -## 7. Create .env file - -```bash -cat > .env << 'EOF' -DJANGO_SETTINGS_MODULE="django_bpp.settings.local" -DEBUG=true -DJANGO_BPP_HOSTNAME="localhost" -DJANGO_BPP_SECRET_KEY="test-secret-key-for-local-dev" -DJANGO_BPP_DB_NAME="bpp" -DJANGO_BPP_DB_USER="bpp" -DJANGO_BPP_DB_PASSWORD="" -DJANGO_BPP_DB_HOST="localhost" -DJANGO_BPP_DB_PORT="5432" -DJANGO_BPP_DB_DISABLE_SSL=1 -DJANGO_BPP_REDIS_HOST="localhost" -DJANGO_BPP_REDIS_PORT="6379" -DJANGO_BPP_REDIS_DB_CELERY="2" -DJANGO_BPP_REDIS_DB_SESSION="4" -DJANGO_BPP_REDIS_DB_CACHE="5" -DJANGO_BPP_REDIS_DB_LOCKS="6" -DJANGO_BPP_RABBITMQ_HOST="localhost" -DJANGO_BPP_RABBITMQ_PORT="5672" -DJANGO_BPP_RABBITMQ_USER="bpp" -DJANGO_BPP_RABBITMQ_PASS="bpp" -DJANGO_BPP_UZYWAJ_PUNKTACJI_WEWNETRZNEJ="0" -DJANGO_BPP_PUNKTUJ_MONOGRAFIE="0" -DJANGO_BPP_INLINE_DLA_AUTOROW="stacked" -DJANGO_BPP_THEME_NAME="app-blue" -DJANGO_BPP_SESSION_SECURITY_WARN_AFTER="25200" -DJANGO_BPP_SESSION_SECURITY_EXPIRE_AFTER="28800" -DJANGO_BPP_PASSWORD_DURATION_SECONDS="315360000" -DJANGO_BPP_USE_PASSWORD_HISTORY="1" -DJANGO_BPP_PASSWORD_HISTORY_COUNT="1" -EOF -``` - -## 8. Install Python dependencies - -```bash -uv sync --all-extras -``` - -**psycopg2 workaround:** Even with `libpq-dev` installed, -`uv sync` may still try to build `psycopg2` from source and -fail. If so, install `psycopg2-binary` and create a fake -dist-info so uv considers the dependency satisfied: - -```bash -uv pip install psycopg2-binary==2.9.11 - -SITE_PKG=$(.venv/bin/python -c \ - "import site; print(site.getsitepackages()[0])") -mkdir -p "$SITE_PKG/psycopg2-2.9.11.dist-info" - -cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/METADATA" << 'EOF' -Metadata-Version: 2.1 -Name: psycopg2 -Version: 2.9.11 -EOF - -cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/RECORD" << 'EOF' -psycopg2/__init__.py,sha256=, -psycopg2-2.9.11.dist-info/METADATA,sha256=, -psycopg2-2.9.11.dist-info/RECORD,, -EOF - -echo "uv" > \ - "$SITE_PKG/psycopg2-2.9.11.dist-info/INSTALLER" - -cat > "$SITE_PKG/psycopg2-2.9.11.dist-info/WHEEL" << 'EOF' -Wheel-Version: 1.0 -Generator: manual -Root-Is-Purelib: false -Tag: cp311-cp311-linux_x86_64 -EOF -``` - -## 9. Run tests (without Playwright) +## Running tests Playwright browsers cannot be downloaded in the sandbox (cdn.playwright.dev is not whitelisted). Run tests excluding @@ -153,19 +13,24 @@ Playwright tests: uv run pytest -m "not playwright" --maxfail 50 ``` -**Known sandbox-specific test failures** (not real bugs): -- Tests using VCR cassettes for external APIs (CrossRef, - DOI) fail because the egress proxy changes the host/port - in requests. These tests pass on a normal machine. +**Full test run (without Playwright) takes ~55 minutes** in +the sandbox environment on a single worker. + +## Known sandbox-specific test failures + +These are **not real bugs** — they pass on a normal machine: + +- Tests using VCR cassettes for external APIs (CrossRef, DOI) + fail because the egress proxy changes the host/port in + requests. - Affected test files: - `src/bpp/tests/test_admin/test_crossref_api_helpers.py` - `src/bpp/tests/test_admin/test_crossref_api_sync.py` - `src/importer_publikacji/tests/test_views.py` - `src/pbn_import/tests/test_admin_compression.py` -**Note:** Docker builds (`make build`, `docker compose up`) -do not work in the sandbox because Docker image layer CDN -(`*.r2.cloudflarestorage.com`) is not in the proxy allowlist. +## Docker -**Full test run (without Playwright) takes ~55 minutes** in -the sandbox environment on a single worker. +Docker builds (`make build`, `docker compose up`) do not work +in the sandbox because Docker image layer CDN +(`*.r2.cloudflarestorage.com`) is not in the proxy allowlist. diff --git a/src/bpp/tests/test_admin/test_crossref_api_sync_playwright.py b/src/bpp/tests/test_admin/test_crossref_api_sync_playwright.py index 81c803cce..5b77d4068 100644 --- a/src/bpp/tests/test_admin/test_crossref_api_sync_playwright.py +++ b/src/bpp/tests/test_admin/test_crossref_api_sync_playwright.py @@ -18,7 +18,10 @@ def autor_m(): ) -@pytest.mark.vcr(ignore_localhost=True) +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query"), + ignore_localhost=True, +) def test_crossref_api_autor_sync( admin_page: Page, live_server, transactional_db, autor_m ): @@ -81,7 +84,10 @@ def admin_page_strona_porownania(admin_page: Page, live_server): return admin_page -@pytest.mark.vcr(ignore_localhost=True) +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query"), + ignore_localhost=True, +) @pytest.mark.parametrize( "id_przycisku, atrybut, wynik", [ @@ -119,7 +125,10 @@ def check_attribute(): ) -@pytest.mark.vcr(ignore_localhost=True) +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query"), + ignore_localhost=True, +) def test_crossref_api_streszczenie_sync_browser( transactional_db, wydawnictwo_ciagle_jehs_2022, diff --git a/src/crossref_bpp/tests/test_admin_helpers.py b/src/crossref_bpp/tests/test_admin_helpers.py index 2cd9b9295..5b067a6d7 100644 --- a/src/crossref_bpp/tests/test_admin_helpers.py +++ b/src/crossref_bpp/tests/test_admin_helpers.py @@ -4,7 +4,9 @@ from crossref_bpp.models import CrossrefAPICache -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_convert_crossref_to_changeform_initial_data_only_e_issn(): z = CrossrefAPICache.objects.get_by_doi("10.3390/ijms24043114") @@ -13,7 +15,9 @@ def test_convert_crossref_to_changeform_initial_data_only_e_issn(): assert ret["issn"] is None -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_convert_crossref_to_changeform_initial_data_both_issns(): z = CrossrefAPICache.objects.get_by_doi("10.3390/ijms24043114") @@ -23,7 +27,9 @@ def test_convert_crossref_to_changeform_initial_data_both_issns(): assert ret["issn"] == "1234-5678" -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_convert_crossref_to_changeform_initial_data_non_electronic_issn(): z = CrossrefAPICache.objects.get_by_doi("10.3390/ijms24043114") diff --git a/src/crossref_bpp/tests/test_forms.py b/src/crossref_bpp/tests/test_forms.py index 9e6095a4c..0795af464 100644 --- a/src/crossref_bpp/tests/test_forms.py +++ b/src/crossref_bpp/tests/test_forms.py @@ -4,7 +4,9 @@ from crossref_bpp.models import CrossrefAPICache -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_PobierzZCrossrefAPIFrom_clean(): assert CrossrefAPICache.objects.count() == 0 diff --git a/src/crossref_bpp/tests/test_views.py b/src/crossref_bpp/tests/test_views.py index 92cffc51a..54056bb9b 100644 --- a/src/crossref_bpp/tests/test_views.py +++ b/src/crossref_bpp/tests/test_views.py @@ -6,7 +6,9 @@ from pbn_api.tests.utils import middleware -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_pobierz_z_crossref_nie_ma_rekordu(rf, admin_user, jezyki): req = rf.post("/", {"identyfikator_doi": "10.1080/1042819021000006394"}) @@ -17,7 +19,9 @@ def test_pobierz_z_crossref_nie_ma_rekordu(rf, admin_user, jezyki): assert "brak takiego DOI w bazie" in ret.rendered_content -@pytest.mark.vcr +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query") +) @pytest.mark.django_db def test_pobierz_z_crossref_form_errors(rf, admin_user): req = rf.post("/", {"identyfikator_doi": "aisjdfoasjdofaijsd"}) diff --git a/src/importer_publikacji/tests/test_playwright_authors.py b/src/importer_publikacji/tests/test_playwright_authors.py index 41d3158eb..1399d0d1d 100644 --- a/src/importer_publikacji/tests/test_playwright_authors.py +++ b/src/importer_publikacji/tests/test_playwright_authors.py @@ -42,7 +42,10 @@ def zrodlo_blood(db): return baker.make(Zrodlo, nazwa="Blood", issn="0006-4971") -@pytest.mark.vcr(ignore_localhost=True) +@pytest.mark.vcr( + match_on=("method", "scheme", "path", "query"), + ignore_localhost=True, +) def test_import_crossref_doi_author_modal_single_open( importer_page: Page, live_server,