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:
@@ -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.
|
||||||
|
|||||||
@@ -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 ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
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(
|
||||||
"Admin password unknown. Set PIC_ADMIN_PASS env var or run: "
|
"Admin password unknown. Set PIC_ADMIN_PASS env var or run: "
|
||||||
"make reset-test-admin-pass PIC_TEST_ADMIN_PASS=<password>"
|
"make reset-test-admin-pass PIC_TEST_ADMIN_PASS=<password>"
|
||||||
|
|||||||
@@ -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'
|
if os.path.exists(candidate):
|
||||||
)
|
return open(candidate).read().strip()
|
||||||
candidate = os.path.normpath(candidate)
|
|
||||||
if os.path.exists(candidate):
|
|
||||||
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>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user