fix: e2e/integration test infrastructure and Makefile test targets

- Fix make test: was pointing to non-existent api/tests/, now runs unit tests
  correctly with --ignore=e2e --ignore=integration
- Remove dead phase test targets (test-phase1..4, test-all-phases) that all
  referenced cd api && pytest tests/ (non-existent path)
- Add .test_admin_pass file: reset_admin_password.py now writes a persistent
  test password file alongside .admin_initial_password; the API never deletes
  it (unlike .admin_initial_password which is consumed on first startup)
- Update both integration/conftest.py and e2e/helpers/admin_password.py to
  read .test_admin_pass before .admin_initial_password — so tests work after
  make restart without needing PIC_ADMIN_PASS env var
- Add AI collaboration rules to CLAUDE.md (auto-loaded every session)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 08:27:27 -04:00
parent 420dced9ff
commit 9677755b4f
5 changed files with 46 additions and 37 deletions
+11
View File
@@ -74,3 +74,14 @@ Config files for each service live under `config/<service>/`. Persistent data is
## Testing ## Testing
Tests live in `tests/` (28 files). Use mocking (`pytest-mock`) for external system calls. Integration tests in `test_integration.py` require Docker services running. Tests live in `tests/` (28 files). Use mocking (`pytest-mock`) for external system calls. Integration tests in `test_integration.py` require Docker services running.
## AI Collaboration Rules (Claude Code)
These rules apply to every Claude Code session in this repo:
- **Read memory first** — load `/home/roof/.claude/projects/-home-roof/memory/MEMORY.md` and referenced files at session start.
- **Dev machine context** — you are already on pic0 (192.168.31.51), the dev machine. Execute commands here directly; do not ask the user to run them.
- **Use all available agents** — spawn specialized sub-agents (pic-remote, pic-qa, pic-architect, etc.) for tasks that match their description.
- **make is the only interface** — never call docker/docker-compose directly. All container lifecycle operations go through `make start`, `make stop`, `make build`, `make logs`, etc.
- **Test every new feature** — after implementing any change, run `make test` before considering the task done.
- **Test before commit** — the pre-commit hook enforces this, but run `make test` manually first and fix all failures before staging files.
+11 -23
View File
@@ -223,17 +223,18 @@ restore:
# ── Tests ───────────────────────────────────────────────────────────────────── # ── Tests ─────────────────────────────────────────────────────────────────────
test: test:
@echo "Running all tests..." @echo "Running unit tests..."
pytest tests/ api/tests/ python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
test-all: test-all: test test-integration test-e2e-api test-e2e-ui
python3 api/tests/run_tests.py @echo "All test suites complete."
test-unit: test-unit:
pytest tests/ python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration -q
test-coverage: test-coverage:
pytest tests/ api/tests/ --cov=api --cov-report=html --cov-report=term-missing --cov-fail-under=70 -v python3 -m pytest tests/ --ignore=tests/e2e --ignore=tests/integration \
--cov=api --cov-report=html --cov-report=term-missing --cov-fail-under=70 -v
test-integration: test-integration:
@echo "Running full integration tests (requires running PIC stack)..." @echo "Running full integration tests (requires running PIC stack)..."
@@ -244,14 +245,15 @@ test-integration-readonly:
PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/test_live_api.py tests/integration/test_webui.py -v PIC_HOST=$${PIC_HOST:-localhost} python3 -m pytest tests/integration/test_live_api.py tests/integration/test_webui.py -v
test-api: test-api:
cd api && python3 -m pytest tests/test_api_endpoints.py -v python3 -m pytest tests/test_api_endpoints.py -v
test-cli: test-cli:
cd api && python3 -m pytest tests/test_cli_tool.py -v python3 -m pytest tests/test_cli_tool.py -v
# ── E2E tests ───────────────────────────────────────────────────────────────── # ── E2E tests ─────────────────────────────────────────────────────────────────
# Run `make test-e2e-deps` once to install dependencies, then use the other targets. # Run `make test-e2e-deps` once to install dependencies, then use the other targets.
# WG tests require wg-quick and run under sudo (passwordless sudo assumed on this host). # Admin password is read from data/api/.test_admin_pass (written by reset-test-admin-pass).
# Override: make test-e2e-api PIC_ADMIN_PASS=mypassword
test-e2e-deps: test-e2e-deps:
sudo pip3 install --break-system-packages -r tests/e2e/requirements.txt sudo pip3 install --break-system-packages -r tests/e2e/requirements.txt
@@ -282,20 +284,6 @@ show-admin-password:
reset-admin-password: reset-admin-password:
@sudo python3 scripts/reset_admin_password.py --generate @sudo python3 scripts/reset_admin_password.py --generate
test-phase1:
cd api && python3 -m pytest tests/test_network_manager.py tests/test_phase1_endpoints.py -v
test-phase2:
cd api && python3 -m pytest tests/test_wireguard_manager.py tests/test_phase2_endpoints.py -v
test-phase3:
cd api && python3 -m pytest tests/test_phase3_managers.py tests/test_phase3_endpoints.py -v
test-phase4:
cd api && python3 -m pytest tests/test_phase4_routing.py tests/test_phase4_endpoints.py -v
test-all-phases:
cd api && python3 -m pytest tests/ -v
# ── Network / peers ─────────────────────────────────────────────────────────── # ── Network / peers ───────────────────────────────────────────────────────────
+8 -1
View File
@@ -15,6 +15,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api'))
ROOT = os.path.join(os.path.dirname(__file__), '..') ROOT = os.path.join(os.path.dirname(__file__), '..')
INIT_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.admin_initial_password')) INIT_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.admin_initial_password'))
TEST_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.test_admin_pass'))
def _generate_password(length: int = 20) -> str: def _generate_password(length: int = 20) -> str:
@@ -88,13 +89,19 @@ def main() -> None:
_set_password(password) _set_password(password)
# Also update the initial password file so show-admin-password works # Write the initial password file (API reads it on first start, then deletes it)
os.makedirs(os.path.dirname(INIT_PW_FILE), exist_ok=True) os.makedirs(os.path.dirname(INIT_PW_FILE), exist_ok=True)
with open(INIT_PW_FILE, 'w') as f: with open(INIT_PW_FILE, 'w') as f:
f.write(password) f.write(password)
# Write the persistent test password file (never deleted by the API)
with open(TEST_PW_FILE, 'w') as f:
f.write(password)
os.chmod(TEST_PW_FILE, 0o600)
_print_banner(password) _print_banner(password)
print(f'\n Also saved to: {INIT_PW_FILE}') print(f'\n Also saved to: {INIT_PW_FILE}')
print(f' Test file: {TEST_PW_FILE} (persists across API restarts)')
print(' Restart the API container for the change to take effect:') print(' Restart the API container for the change to take effect:')
print(' docker restart cell-api') print(' docker restart cell-api')
+5 -3
View File
@@ -1,13 +1,15 @@
import os import os
_DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'api'))
def resolve_admin_password() -> str: def resolve_admin_password() -> str:
p = os.environ.get('PIC_ADMIN_PASS', '').strip() p = os.environ.get('PIC_ADMIN_PASS', '').strip()
if p: if p:
return p return p
candidate = os.path.normpath( for fname in ('.test_admin_pass', '.admin_initial_password'):
os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'api', '.admin_initial_password') candidate = os.path.join(_DATA_DIR, fname)
)
if os.path.exists(candidate): if os.path.exists(candidate):
return open(candidate).read().strip() return open(candidate).read().strip()
raise RuntimeError( raise RuntimeError(
+7 -6
View File
@@ -35,18 +35,19 @@ TEST_PEERS = (
TEST_PEER_PASSWORD = 'IntegrationTest123!' TEST_PEER_PASSWORD = 'IntegrationTest123!'
_DATA_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'api'))
def _resolve_admin_pass() -> str: def _resolve_admin_pass() -> str:
if ADMIN_PASS: if ADMIN_PASS:
return ADMIN_PASS return ADMIN_PASS
# Try reading from the initial password file (present on first run before bootstrap) for fname in ('.test_admin_pass', '.admin_initial_password'):
candidate = os.path.join( candidate = os.path.join(_DATA_DIR, fname)
os.path.dirname(__file__), '..', '..', 'data', 'api', '.admin_initial_password'
)
candidate = os.path.normpath(candidate)
if os.path.exists(candidate): if os.path.exists(candidate):
return open(candidate).read().strip() return open(candidate).read().strip()
raise RuntimeError( raise RuntimeError(
"Admin password unknown. Set PIC_ADMIN_PASS env var or run make setup first." "Admin password unknown. Set PIC_ADMIN_PASS env var or run: "
"make reset-test-admin-pass PIC_TEST_ADMIN_PASS=<password>"
) )