Make lint report instead of rewriting, and stop verify guessing the domain

`make lint-frontend` was `biome check --write --unsafe ./` — a lint target that
reformatted the whole tree rather than checking it, which is why every parallel
change in this repo has had to work around it. `lint` checks now and a new
`format` writes. The pre-commit hook and CI needed no edit at all: both call
`bun run lint`, so they became checks the moment its meaning changed.

`app/Makefile` assigned DOMAIN from .env, and a plain assignment beats an
inherited environment variable and is not exported — so `cd app && make
dev-local` served localhost while the same checkout's tests targeted the
configured domain. `export DOMAIN ?=` gives the lattice that was intended:
command line, then environment, then .env.

Alongside: the backend's htmlcov bind mount created that directory as root, so
`make test-backend` died on the coverage step after every test had passed,
which reads like a test failure and is not one. The alerts screen's copy of
ALERTING_EVENTS is now checked by a test rather than trusted. And the shard
comment claimed two spec files where there are nine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
2026-08-21 14:34:14 +02:00
co-authored by Claude Opus 5
parent d9a1eeb3b6
commit 7da3bbdc32
9 changed files with 46 additions and 20 deletions
+4 -3
View File
@@ -37,9 +37,10 @@ jobs:
strategy:
fail-fast: false
matrix:
# Two spec files (tests/flows.spec.ts, tests/admin.spec.ts) plus
# tests/auth.setup.ts, which every shard reruns as its setup project.
# More shards than that would only pay container startup twice over.
# Nine spec files under tests/, plus tests/auth.setup.ts, which every
# shard reruns as its setup project. Split two ways: each shard pays a
# full compose stack's startup, so more of them would spend more time
# booting than testing.
shardIndex: [1, 2]
shardTotal: [2]
env:
+5 -2
View File
@@ -4,7 +4,7 @@
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \
generate-client seed-example seed-demo seed-house seed-aircon seed-hosted-demo test test-backend test-frontend soak bench-startup lint lint-backend \
lint-frontend umami clean help
lint-frontend format-frontend umami clean help
COMPOSE_ROOT := $(CURDIR)
# Explicit project name keeps this stack isolated from the sibling website
@@ -133,7 +133,7 @@ test-backend: ## Run backend tests (pytest + coverage)
# deployment carries the deployment's domain, and *.fluksio.com resolves to the
# live instance from here — which is why the run below maps both names onto the
# local Traefik by address and never lets DNS decide.
DOMAIN = $(shell sed -n 's/^DOMAIN=//p' $(COMPOSE_ROOT)/.env | head -1)
export DOMAIN ?= $(shell sed -n 's/^DOMAIN=//p' $(COMPOSE_ROOT)/.env | head -1)
PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/p' frontend/package.json | head -1)
test-frontend: ## Run frontend tests (Playwright e2e) against the local stack
@@ -174,6 +174,9 @@ lint-backend: ## Lint backend with ruff + mypy
lint-frontend: ## Lint frontend with biome
cd frontend && bun run lint
format-frontend: ## Apply biome's fixes to the frontend (what `lint-frontend` only reports)
cd frontend && bun run format
# ── Cleanup ───────────────────────────────────────────────────────
clean: ## Remove build artifacts and caches
+17
View File
@@ -1,10 +1,13 @@
"""Alerting: failing loudly once, not thirty-six thousand times."""
import asyncio
import re
from pathlib import Path
import pytest
from app.flow.alerts import (
ALERTING_EVENTS,
FLAP_THRESHOLD,
RATE_LIMIT,
Alert,
@@ -230,3 +233,17 @@ def test_a_dashboard_channel_without_a_message_says_so():
asyncio.run(
alerts.send(channel, Alert(title="t", body="b"), raise_on_error=True)
)
ALERTS_ROUTE = Path(__file__).parents[3] / "frontend/src/routes/_layout/alerts.tsx"
@pytest.mark.skipif(not ALERTS_ROUTE.exists(), reason="no frontend in this checkout")
def test_the_alerts_screen_offers_every_alerting_event():
"""The chooser lists the events by hand, so it can drift out of this set.
Only the ids have to agree — the labels beside them are UI copy.
"""
block = re.search(r"const EVENTS[^=]*= \[(.*?)\n\]", ALERTS_ROUTE.read_text(), re.S)
assert block, "the EVENTS list moved — this test needs following"
assert set(re.findall(r'\["(\w+)"', block.group(1))) == ALERTING_EVENTS
+12 -9
View File
@@ -14,9 +14,9 @@ SPA, the API and the marketing site answer on one port. `make status` lists the
`make down` stops everything.
The hostname comes from `DOMAIN` in `app/.env`, which `scripts/setup.sh` copies out of the
root `.env` — change it there and re-run `make init`. `make dev DOMAIN=…` at the root does
*not* reach this stack: `app/Makefile` assigns `DOMAIN` from `app/.env`, and a makefile
assignment overrides an inherited environment variable.
root `.env` — change it there and re-run `make init`. `app/Makefile` takes that value as a
default only (`export DOMAIN ?= …`), so `make dev DOMAIN=…` overrides it and the override
reaches the recipes.
**App only.** `cd app && make dev` starts this stack on its own Traefik with all host ports
published, in the foreground. `make dev-utils` starts just db, adminer, proxy, mailcatcher
@@ -145,15 +145,18 @@ run when either origin resolves outside loopback or the private ranges, because
creates and deletes flows, dashboards and users. `PLAYWRIGHT_ALLOW_PUBLIC=1` overrides it.
**Visual check.** From the root, `make verify` logs in and screenshots both themes into
`app/frontend/screenshots/{light,dark}/`; `make verify-docker` does the same from the
Playwright container when the host lacks the browser libraries. Both default to `localhost`
rather than reading `.env`, so on a checkout configured for a deployment name the domain the
stack actually serves: `make verify DOMAIN=example.com`.
`app/frontend/screenshots/{light,dark}/`. It is an alias for `make verify-docker`: the run
always happens in the Playwright container, which pins the browser by reading
`frontend/package.json`. Running Playwright on the host is unsupported — the workspace pins a
browser revision whose install is incomplete. The domain comes from `.env`, and
`make verify DOMAIN=example.com` still wins.
## Lint, hooks and the generated client
`make lint` runs ruff, mypy and biome. `make hooks` at the root installs the pre-commit
hooks in both stacks; `.pre-commit-config.yaml` holds them. One is worth knowing about:
`make lint` runs ruff, mypy and biome, and only reports — it is what the pre-commit hook and
CI run, so neither rewrites the tree. `make format-frontend` is the writing half, applying
biome's fixes. `make hooks` at the root installs the pre-commit hooks in both stacks;
`.pre-commit-config.yaml` holds them. One is worth knowing about:
`generate-frontend-sdk` regenerates the SPA's API client from the backend's OpenAPI schema
whenever anything under `backend/` changes, so a route change reaches the frontend without
being asked. `make generate-client` forces it.
-1
View File
@@ -105,7 +105,6 @@ services:
- path: ../backend/pyproject.toml
action: rebuild
volumes:
- ../backend/htmlcov:/app/backend/htmlcov
# What makes --reload above mean anything in the detached flow (`make dev`,
# `make dev-local`): it starts the stack with `up -d`, which never runs the
# sync above — only `docker compose watch` does. Without the mount the
+2 -1
View File
@@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "tsc -p tsconfig.build.json && vite build",
"lint": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
"lint": "biome check --no-errors-on-unmatched --files-ignore-unknown=true ./",
"format": "biome check --write --unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
"preview": "vite preview",
"generate-client": "openapi-ts",
"test": "bunx playwright test",
+4 -3
View File
@@ -35,9 +35,10 @@ const alertsKey = ["alerts", "config"]
const SECTION =
"text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground"
/** What the engine can alert on. Mirrors `ALERTING_EVENTS` in the backend; a
* rule with none of them ticked still covers everything, including any event
* added there later. */
/** What the engine can alert on. Mirrors `ALERTING_EVENTS` in the backend, and
* `backend/tests/flow/test_alerts.py` reads this list to fail when the two
* drift apart. A rule with none of them ticked still covers everything,
* including any event added there later. */
const EVENTS: [string, string][] = [
["node_error", "A node failed"],
["node_health", "A connection dropped"],
+1
View File
@@ -7,6 +7,7 @@
"scripts": {
"dev": "bun run --filter frontend dev",
"lint": "bun run --filter frontend lint",
"format": "bun run --filter frontend format",
"test": "bun run --filter frontend test",
"test:ui": "bun run --filter frontend test:ui"
}
+1 -1
View File
@@ -8,4 +8,4 @@ uv run python -c "import app.main; import json; print(json.dumps(app.main.app.op
cd ..
mv openapi.json frontend/
bun run --filter frontend generate-client
bun run lint
bun run format