The coverage export from `test.coverage` now also feeds a `crap` sensor,
second in app's VERIFY_SENSORS. It runs with MIX_ENV=test so it reads the
export's own environment instead of compiling a dev build and scoring a
different module set, and it guards on the export's existence because
ex_crap silently falls back to 0% coverage when the file is missing.
Baseline at ex_crap's default threshold (30): 23 files, 139 functions,
worst score 20.00 (CoreComponents.table/1, 0% - unused upstream scaffold).
No offenders, so nothing to refactor or cover yet.
The skill assumed port 4000, but config/dev.exs serves on 8056 (and honours
$PORT), so a fresh demo run failed its liveness check and every rodney URL
pointed at the wrong port.
- demo skill: resolve DEMO_PORT from the app config with
`mix run --no-start -e 'Application.get_env(:firehose,
FirehoseWeb.Endpoint)[:http][:port]'` (Endpoint.config/1 raises without a
started endpoint), derive $DEMO_BASE_URL from it, start the server on that
port from app/, and use $DEMO_BASE_URL in every rodney example. The skill
also points at .tmp/phx-demo.log and names the assets/node_modules
highlight.js failure as a diagnosable cause.
- app/Makefile: new `assets` target that runs `npm install` only when
node_modules/.package-lock.json is missing, and `deps` now depends on it.
A fresh clone gets a working JS bundle from `make deps`/`make test`; later
runs never touch the network, so offline runs keep working.
Verified: guard skips with node_modules present and installs after moving it
aside; the skill's DEMO_PORT command prints 8056 and follows PORT=9999;
make check and make test (179 + 162) green.
External links in post bodies already opened in a new tab and showed an
icon, but the icon is invisible to screen readers, so nothing announced
that the link leaves the site.
- blogex: ExternalLinks.rewrite/1 appends
<span class="blogex-external-hint"> (opens in a new tab)</span> as the
last child of every external anchor. No aria-label, so the accessible
name still contains the visible link text (WCAG 2.5.3); the icon colour
and 2.5.3 both hold. Idempotent: a second pass adds no second hint.
- blogex: anchors are now matched together with their closing tag
(the hint goes inside), so a stray unclosed <a> is left alone.
- app: sr-only rule for .blogex-external-hint in .blogex-post-body. The
split stays as before: blogex marks up, the host app styles.
- tests: hint cases in blogex (appended, per link, after inner markup,
idempotent, not on internal/mailto/anchor links) + anchors without href
(Blogex.ExternalLinks now at 100% line coverage), and an app test that
the hint reaches the rendered post page. Two exact-string expectations
updated for the intended markup change.
make test: app 179, blogex 162. make verify: green.
Raises the measured total 86.17% -> 86.79% without touching a test, because the
excluded modules hold no behavior a test can observe:
- FirehoseWeb.{Blog,UserRegistration,UserSettings}HTML are `use` +
`embed_templates` only; every function in them is macro-generated. Their
TEMPLATES are already exercised through the controller tests.
- Firehose.Application: start/2 is proven by the suite booting at all, and
config_change/3 runs only on a release hot upgrade.
Deliberately NOT excluded, despite being embed_templates wrappers:
FirehoseWeb.PageHTML (blog_label/1, post_base_path/1) and
FirehoseWeb.UserSessionHTML (local_mail_adapter?/0) carry real private logic, so
their sub-100% coverage is a behavior gap for the controller/auth yak, not macro
noise.
Also drops the now-dead Firehose.Checks.NoConnShadowing entry: lib_dev/ is no
longer compiled, so it is not measurable. Floor stays 86 (86.79 rounds down).
lib_dev/ was both compiled (elixirc_paths for :dev and :test) and source-loaded
by credo through `requires:` in .credo.exs. Two load paths for one module means
that whenever _build/dev already holds the beam, `mix credo` warned:
warning: redefining module Firehose.Checks.NoConnShadowing
(current version loaded from _build/dev/.../Elixir.Firehose.Checks.NoConnShadowing.beam)
Keep the explicit contract and drop the accidental one: `requires:` is the
documented way for credo to load project checks, so lib_dev/ now leaves
elixirc_paths in every environment. Nothing under test/ referenced the check, so
the :test entry was dead as well, and credo compiles the file on every run
anyway, so the check still gets compiled — just once.
One-time migration for checkouts that already built it:
`mix clean` (or delete _build/dev) to drop the stale beam; until then the old
warning persists. Fresh clones are unaffected.
These two come from the upstream Phoenix generator and are worth keeping for
future data/admin screens, so they stay rather than being deleted as dead code.
The note also records why they look unused but aren't pointless: the collections
already in the UI are hand-rolled (editor_dashboard_live's drafts/scheduled rows
are bordered divs; Blogex.Components.post_index renders post previews and tag
lists itself). That is the fact a future reader needs before either deleting
these or writing yet another bespoke list.
Measured before choosing: 86.17%, identical via `mix test --cover` and via the
exported sample + `mix test.coverage`, stable across seeds. Floor is the integer
below that, with the rule written next to it: a FLOOR, ratcheted up as
behavior-test yaks land, never lowered (a drop means the change or an exclusion
is wrong).
Exclusions are the modules the suite cannot or should not exercise, each
justified in the config comment: `use`-generated glue (Repo, Mailer, Telemetry,
ErrorHTML/ErrorJSON), the `use FirehoseWeb` macro, production release tasks
(Release), the lib_dev credo check (NoConnShadowing), and test scaffolding
(DataCase, ConnCase, AccountsFixtures, Test.FakeBlog). Everything else counts,
CoreComponents/controllers/LiveViews included.
Wiring matches blogex and the fast/push split: `test.coverage` runs the suite
once with `--cover --export-coverage default` (deleting stale exports first,
since `mix test.coverage` imports every .coverdata it finds and would pool
them), then the `coverage` sensor enforces the floor from that export. The
sensor guards on the export existing because `mix test.coverage` exits 0 when it
finds nothing — a floor check that did not run must not pass.
`redact: true` makes Ecto derive an Inspect impl at the `schema` line.
Elixir 1.20.4 emits that whole derived impl on that one line, and OTP 27's
coverage instrumentation (sys_coverage) turns it into invalid Erlang
("variable '_2' unsafe in 'case'"), so `mix test --cover` aborted for the whole
app before reporting anything. Minimal repro, no Ecto needed:
`@derive Inspect` on a plain struct fails on Elixir 1.20.4 + OTP 27.3.4.19 and
works on Elixir 1.18.3 + the same OTP.
Writing the impl out by hand, one expression per line, keeps `inspect/1`
output byte-identical (Ecto/Elixir render redacted fields as
`#Mod<..., ...>`) and keeps redaction pinned by the existing test, and lets the
coverage sensor land. Revisit when the Elixir/OTP pins change.
Root verify runs check first, then a per-app verify in each Makefile: sensor
slots hooked to VERIFY_SENSORS (empty today, so nothing is claimed that is not
run) and then that app's suite. Measured: 9.9s wall, app 178 + blogex 156 tests.
mise now lives at /opt/homebrew/bin/mise (Homebrew install), so the
hardcoded MISE_BIN default broke make test/deps/compile. Fall back to
PATH lookup, matching what the check target already did.
The microprints git dep now allows phoenix_live_view ~> 1.2, so the
app can take the CVE-2026-64941 fix (open redirect in
validate_local_url!/2, fixed in 1.2.9). No 1.1.x backport existed.
blogex test lock follows to 1.2.12 so the library is tested against
the version the app ships with. mix deps.get now reports zero
security advisories.
- Restore HTML link extraction in LinkValidator (removed in a83634d
under the false premise that post bodies are raw markdown; they
are HTML rendered by NimblePublisher at compile time). The missing
regex made extract_links/1 find zero links, silently disabling
compile-time validation.
- Support /blog/{blog_id}/tag/{tag} links: validate blog ID,
require non-empty tag (tags are user-defined, e.g. pi.dev).
- Fix invalid links in two posts: tag/Pi.dev -> tag/pi.dev,
2026-07-13-synthetic-tdd.md -> synthetic-tdd.
- Fix test warnings: use Plug.Test deprecation, unused import,
runtime-defined TestBlogValid module.
- Add regression tests for HTML extraction and tag page links.
External links in post bodies now open in a new tab and show a
box-with-arrow marker icon, so readers can tell at a glance which
links leave the site.
- blogex: new Blogex.ExternalLinks.rewrite/1 marks up anchors with
http(s) hrefs at compile time with target="_blank", rel="noopener"
and class="blogex-external" (idempotent, merges into existing
class/rel values, leaves relative, mailto: and #anchor links alone)
- blogex: Post.build/3 applies the rewrite, so every blog gets it
- app: CSS in .blogex-post-body draws the marker via a mask so it
inherits the link colour; empty ::after content stays invisible
to screen readers
- make test now also runs the blogex suite; it is currently red
with 3 pre-existing blog_integration_test failures caused by the
vacuous link validator (parked yak
link-validator-never-sees-compiled-html-...)