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 b0efb4b0f1
commit c912388ed6
9 changed files with 46 additions and 20 deletions
+4 -3
View File
@@ -37,9 +37,10 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Two spec files (tests/flows.spec.ts, tests/admin.spec.ts) plus # Nine spec files under tests/, plus tests/auth.setup.ts, which every
# tests/auth.setup.ts, which every shard reruns as its setup project. # shard reruns as its setup project. Split two ways: each shard pays a
# More shards than that would only pay container startup twice over. # full compose stack's startup, so more of them would spend more time
# booting than testing.
shardIndex: [1, 2] shardIndex: [1, 2]
shardTotal: [2] shardTotal: [2]
env: env:
+5 -2
View File
@@ -4,7 +4,7 @@
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \ .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 \ 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) COMPOSE_ROOT := $(CURDIR)
# Explicit project name keeps this stack isolated from the sibling website # 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 # 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 # live instance from here — which is why the run below maps both names onto the
# local Traefik by address and never lets DNS decide. # 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) 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 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 lint-frontend: ## Lint frontend with biome
cd frontend && bun run lint 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 ─────────────────────────────────────────────────────── # ── Cleanup ───────────────────────────────────────────────────────
clean: ## Remove build artifacts and caches clean: ## Remove build artifacts and caches
+17
View File
@@ -1,10 +1,13 @@
"""Alerting: failing loudly once, not thirty-six thousand times.""" """Alerting: failing loudly once, not thirty-six thousand times."""
import asyncio import asyncio
import re
from pathlib import Path
import pytest import pytest
from app.flow.alerts import ( from app.flow.alerts import (
ALERTING_EVENTS,
FLAP_THRESHOLD, FLAP_THRESHOLD,
RATE_LIMIT, RATE_LIMIT,
Alert, Alert,
@@ -230,3 +233,17 @@ def test_a_dashboard_channel_without_a_message_says_so():
asyncio.run( asyncio.run(
alerts.send(channel, Alert(title="t", body="b"), raise_on_error=True) 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. `make down` stops everything.
The hostname comes from `DOMAIN` in `app/.env`, which `scripts/setup.sh` copies out of the 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 root `.env` — change it there and re-run `make init`. `app/Makefile` takes that value as a
*not* reach this stack: `app/Makefile` assigns `DOMAIN` from `app/.env`, and a makefile default only (`export DOMAIN ?= …`), so `make dev DOMAIN=…` overrides it and the override
assignment overrides an inherited environment variable. reaches the recipes.
**App only.** `cd app && make dev` starts this stack on its own Traefik with all host ports **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 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. 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 **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 `app/frontend/screenshots/{light,dark}/`. It is an alias for `make verify-docker`: the run
Playwright container when the host lacks the browser libraries. Both default to `localhost` always happens in the Playwright container, which pins the browser by reading
rather than reading `.env`, so on a checkout configured for a deployment name the domain the `frontend/package.json`. Running Playwright on the host is unsupported — the workspace pins a
stack actually serves: `make verify DOMAIN=example.com`. 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 ## Lint, hooks and the generated client
`make lint` runs ruff, mypy and biome. `make hooks` at the root installs the pre-commit `make lint` runs ruff, mypy and biome, and only reports — it is what the pre-commit hook and
hooks in both stacks; `.pre-commit-config.yaml` holds them. One is worth knowing about: 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 `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 whenever anything under `backend/` changes, so a route change reaches the frontend without
being asked. `make generate-client` forces it. being asked. `make generate-client` forces it.
-1
View File
@@ -105,7 +105,6 @@ services:
- path: ../backend/pyproject.toml - path: ../backend/pyproject.toml
action: rebuild action: rebuild
volumes: volumes:
- ../backend/htmlcov:/app/backend/htmlcov
# What makes --reload above mean anything in the detached flow (`make dev`, # 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 # `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 # sync above — only `docker compose watch` does. Without the mount the
+2 -1
View File
@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -p tsconfig.build.json && vite build", "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", "preview": "vite preview",
"generate-client": "openapi-ts", "generate-client": "openapi-ts",
"test": "bunx playwright test", "test": "bunx playwright test",
+4 -3
View File
@@ -35,9 +35,10 @@ const alertsKey = ["alerts", "config"]
const SECTION = const SECTION =
"text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground" "text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground"
/** What the engine can alert on. Mirrors `ALERTING_EVENTS` in the backend; a /** What the engine can alert on. Mirrors `ALERTING_EVENTS` in the backend, and
* rule with none of them ticked still covers everything, including any event * `backend/tests/flow/test_alerts.py` reads this list to fail when the two
* added there later. */ * drift apart. A rule with none of them ticked still covers everything,
* including any event added there later. */
const EVENTS: [string, string][] = [ const EVENTS: [string, string][] = [
["node_error", "A node failed"], ["node_error", "A node failed"],
["node_health", "A connection dropped"], ["node_health", "A connection dropped"],
+1
View File
@@ -7,6 +7,7 @@
"scripts": { "scripts": {
"dev": "bun run --filter frontend dev", "dev": "bun run --filter frontend dev",
"lint": "bun run --filter frontend lint", "lint": "bun run --filter frontend lint",
"format": "bun run --filter frontend format",
"test": "bun run --filter frontend test", "test": "bun run --filter frontend test",
"test:ui": "bun run --filter frontend test:ui" "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 .. cd ..
mv openapi.json frontend/ mv openapi.json frontend/
bun run --filter frontend generate-client bun run --filter frontend generate-client
bun run lint bun run format