DevPlace CI / test (push) Failing after 58m59s
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
177 lines
5.9 KiB
Makefile
177 lines
5.9 KiB
Makefile
LOCUST_PORT ?= 10502
|
|
LOCUST_WEB_PORT ?= 10503
|
|
LOCUST_DB_DIR ?= /tmp/devplace_locust
|
|
LOCUST_DB ?= $(LOCUST_DB_DIR)/datastore.db
|
|
LOCUST_USERS ?= 20
|
|
LOCUST_SPAWN_RATE ?= 5
|
|
LOCUST_RUN_TIME ?= 120s
|
|
LOCUST_WEB_WORKERS ?= 4
|
|
WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2)
|
|
DEVPLACE_RATE_LIMIT ?= 1000000
|
|
|
|
PYTHONDONTWRITEBYTECODE := 1
|
|
export PYTHONDONTWRITEBYTECODE
|
|
|
|
.PHONY: install dev clean tree tree-loc zip test test-headed test-unit test-api test-e2e test-fast test-failed test-first-failure test-slowest test-cache-clean coverage coverage-headed coverage-html locust locust-headless
|
|
|
|
install:
|
|
pip install -e .
|
|
python -m playwright install chromium
|
|
|
|
dev:
|
|
uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port 10500 --backlog 4096
|
|
|
|
prod:
|
|
DEVPLACE_STATIC_VERSION=$$(date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(WEB_WORKERS) uvicorn devplacepy.main:app --host 0.0.0.0 --port 10500 --workers $(WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*'
|
|
|
|
delete-pyc:
|
|
find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true
|
|
find . -name "*.pyc" -delete
|
|
|
|
tree:
|
|
git ls-files | tree --fromfile --noreport
|
|
|
|
tree-loc:
|
|
@git ls-files | while IFS= read -r f; do \
|
|
loc=$$(wc -l < "$$f" 2>/dev/null || echo 0); \
|
|
printf '%s [%s LOC]\n' "$$f" "$$loc"; \
|
|
done | tree --fromfile --noreport
|
|
|
|
zip:
|
|
@rm -f $(notdir $(CURDIR)).zip
|
|
@git ls-files -z | xargs -0 zip -q $(notdir $(CURDIR)).zip
|
|
@printf 'Wrote %s (%s files)\n' "$(notdir $(CURDIR)).zip" "$$(git ls-files | wc -l)"
|
|
|
|
test:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/
|
|
|
|
test-headed:
|
|
PLAYWRIGHT_HEADLESS=0 python -m pytest tests/
|
|
|
|
test-unit:
|
|
python -m pytest tests/unit
|
|
|
|
test-api:
|
|
python -m pytest tests/api
|
|
|
|
test-e2e:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/e2e
|
|
|
|
test-fast:
|
|
python -m pytest tests/unit tests/api
|
|
|
|
test-failed:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --last-failed --last-failed-no-failures none
|
|
|
|
test-first-failure:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -x
|
|
|
|
test-slowest:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --durations=40
|
|
|
|
coverage:
|
|
rm -f .coverage .coverage.*
|
|
COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=1 \
|
|
python -m coverage run -m pytest tests/
|
|
python -m coverage combine
|
|
python -m coverage report
|
|
|
|
coverage-headed:
|
|
rm -f .coverage .coverage.*
|
|
COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=0 \
|
|
python -m coverage run -m pytest tests/
|
|
python -m coverage combine
|
|
python -m coverage report
|
|
|
|
coverage-html: coverage
|
|
python -m coverage html
|
|
@echo "Report written to htmlcov/index.html"
|
|
|
|
locust:
|
|
export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
|
|
export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
|
|
fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
|
|
sleep 1; \
|
|
mkdir -p $(LOCUST_DB_DIR); \
|
|
rm -f $(LOCUST_DB); \
|
|
DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
|
|
PID=$$!; \
|
|
while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
|
|
if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
|
|
locust --host http://127.0.0.1:$(LOCUST_PORT) --web-port $(LOCUST_WEB_PORT); \
|
|
kill $$PID 2>/dev/null || true; \
|
|
rm -rf $(LOCUST_DB_DIR)
|
|
|
|
locust-headless:
|
|
export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
|
|
export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
|
|
fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
|
|
sleep 1; \
|
|
mkdir -p $(LOCUST_DB_DIR); \
|
|
rm -f $(LOCUST_DB); \
|
|
DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
|
|
PID=$$!; \
|
|
while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
|
|
if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
|
|
locust --host http://127.0.0.1:$(LOCUST_PORT) --headless -u $(LOCUST_USERS) -r $(LOCUST_SPAWN_RATE) --run-time $(LOCUST_RUN_TIME) --html=$(LOCUST_DB_DIR)/report.html; \
|
|
kill $$PID 2>/dev/null || true; \
|
|
rm -rf $(LOCUST_DB_DIR)
|
|
|
|
clean:
|
|
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type f -name '*.pyc' -delete
|
|
rm -rf devplacepy.egg-info
|
|
rm -rf .pytest_cache
|
|
rm -rf .venv
|
|
|
|
test-cache-clean:
|
|
rm -rf .pytest_cache
|
|
|
|
# Container Manager works out of the box: the overlay installs the docker CLI in
|
|
# the image and mounts the host socket. DOCKER_GID is read straight from the
|
|
# socket so the UID-1000 app can use it; the data dir is the project's own data/
|
|
# at its real host path, so the DooD bind-mount (host == container path) holds
|
|
# with no /srv dir and no sudo.
|
|
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.containers.yml
|
|
DEVPLACE_DATA_DIR ?= $(CURDIR)/data
|
|
DOCKER_GID ?= $(shell stat -c '%g' /var/run/docker.sock 2>/dev/null)
|
|
export DEVPLACE_DATA_DIR
|
|
export DOCKER_GID
|
|
|
|
.PHONY: docker-build docker-up docker-reload docker-down docker-logs docker-clean docker-prep ppy
|
|
|
|
# Build the single shared container image every instance runs. Build once;
|
|
# rebuild only when ppy.Dockerfile, the sudo shim, or pagent change.
|
|
ppy:
|
|
docker build --network=host -f ppy.Dockerfile -t ppy:latest devplacepy/services/containers/files
|
|
|
|
docker-prep:
|
|
mkdir -p $(DEVPLACE_DATA_DIR)
|
|
|
|
docker-build: docker-prep
|
|
$(COMPOSE) build
|
|
|
|
docker-up: docker-prep
|
|
$(COMPOSE) up -d
|
|
|
|
docker-reload:
|
|
$(COMPOSE) restart app
|
|
$(COMPOSE) up -d --wait
|
|
|
|
docker-down:
|
|
$(COMPOSE) down
|
|
|
|
docker-logs:
|
|
$(COMPOSE) logs -f
|
|
|
|
docker-clean:
|
|
$(COMPOSE) down -v
|
|
|
|
docker-bup: docker-build docker-up
|
|
|
|
deploy:
|
|
git checkout production
|
|
git merge master
|
|
git push origin production
|
|
|