The org-roam note exporter and its tests/fixtures now live in the doom
configuration (~/.doom.d/qwan) as org-roam-export-blog.el: renamed to
the org-roam- prefix so M-x org-roam-export-blog is discoverable, with
a configurable repo root and a new jekyll draft target.
Drop the test-elisp make target: it pointed at the moved files.
Some deps download artifacts at compile time: rustler_precompiled
fetches the mdex_native NIF from GitHub during its first compile, and
NimblePublisher pulls mdex_native into both apps. Compiling inside the
deps target keeps that traffic inside setup - the explicit user-run
network step - instead of the first make check/test on a fresh clone.
Commit 1709cb7 deleted the pinned hello-world post and ten tests
silently rotted with it - invisible until now because the suite could
not run in this clone (incomplete deps, then the broken frontmatter).
The slug, banner, og-meta and tag tests now derive slug and title from
the compiled posts at runtime (newest live post, newest draft), matching
the existing latest_post_with_links pattern, so content churn no longer
breaks them.
\+ is a GNU BRE extension; /usr/bin/sed on macOS treats it as a literal
plus, so every Dockerfile ARG pin was reported "not found" and make
check false-failed even though mise.toml and the Dockerfile agree. Use
the POSIX form \{1,\} and note the constraint in the header.
mix deps.get contacts hex.pm even when everything is already installed
(the advisory check), so the everyday loops failed in offline/sandboxed
environments; and the root deps target only installed app's deps, never
blogex's.
make setup (root) installs Hex deps for both apps plus the app's JS
packages, once per clone, explicitly. check, test and test.coverage in
both apps now depend on a local deps.check guard instead: every package
pinned in mix.lock must exist in deps/, else fail loudly pointing at
make setup. AGENTS.md documents the new target.
8fa342e split the description's trailing ", onto its own line, leaving
the frontmatter invalid Elixir; NimblePublisher evals frontmatter at
compile time, so the app could not compile. Also drops the
“/” entities from the description (uncommitted WIP, kept
per plan).
`put_token_in_session/2` wrote only `:user_token`, so `log_out_user/1`'s
`if live_socket_id = get_session(conn, :live_socket_id)` was always false and
logging out left open LiveView sockets running. Write the socket topic
alongside the token, copying phx.gen.auth's format verbatim
("users_sessions:" <> Base.url_encode64(token)) so it matches both what
Phoenix.LiveView.Socket.id/1 reads and what log_out_user/1 reads. The reissue
path goes through the same function, so it follows the new token.
Tests (provider/reader agreement, both previously absent):
- log_in_user/3 stores the id derived from the session token
- fetch_current_scope_for_user/2 reissues the id with the new token
- log_out_user/1 broadcasts exactly one "disconnect" on that id
- the value written is the topic Socket.id/1 returns for the session the
websocket receives (string keys)
Not tested end to end: a real disconnect needs the transport process, which
Phoenix.LiveViewTest does not start — Phoenix.Socket.__info__/2 handles the
broadcast (deps/phoenix/lib/phoenix/socket.ex:571) and
phoenix_live_view/test/client_proxy.ex never subscribes to the socket id topic.
Gate: make check + make verify green (app coverage 86.97%, blogex 75.16%,
crap sensors green).
Yak: logout-must-disconnect-existing-liveview-sockets-livesocketid-never-set-6fme
Same wiring as app: `crap` reads the export from `test.coverage`, runs in
MIX_ENV=test, and refuses to score when the export is absent.
Baseline at the default threshold (30): 12 files, 59 functions, worst score
6.00. Nothing to fix; blogex's CRAP ceiling is its coverage ceiling, which the
behavior-test yaks already own.
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.
Steps 2 and 4 carried copy-paste from another project (lib/hub_web/router.ex,
alias Hub.Accounts, /users/log-in with user[email]). A stale path there sends
the agent to a router that does not exist, and a guessed module alias fails at
run time.
Both are now generic instructions to read the project:
- route map: locate the router by searching for `use Phoenix.Router`, then read
its scopes, pipelines and live_sessions to see what is public and what needs
a session
- authentication: only when the route actually needs it; take credentials from
documented demo accounts or the project's seeds, create a demo user through
the project's own path (seeds, registration route, or the accounts function
found by grep - never a guessed alias), and read the login route and form
selectors off the router and rendered form
The port example keeps its concrete app/endpoint pair, now labelled as an
example with the grep that finds the right names, plus a note to use the
project's toolchain/build wrapper instead of bare `mix`.
make check green.
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.
Three captures of /blog/engineering/delayed-gratification-in-ai-summer
taken with rodney (headless chromium):
- external-link-hint-icon.png: a paragraph as rendered - the marker icon
follows the external link, the hint text is invisible
- external-link-hint-unhidden.png: same paragraph with the sr-only rule
cancelled, showing " (opens in a new tab)" inside the link, which is
what screen readers get
- external-link-hint-page.png: full page for context (internal links such
as "Swim the Test" carry no icon)
Backing evidence, from the same load: the accessibility tree names exactly
the 8 external links with "opens in a new tab"; the hint box measures
1x1 with clip-path inset(50%).
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.
Same shape in both apps now that coverage is wired: one suite run exports the
coverage, then the sensors read it. AGENTS.md said "sensors ... and finally both
test suites" and told sensor authors only to list targets in VERIFY_SENSORS; add
the export dependency and the "a check must fail when its input is missing" rule,
since `mix test.coverage` exits 0 when it finds no export.
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.
`test_coverage` sets the floor the Lens pass enforces, measured first: 74.53%
with test/support excluded (`mix test --cover` and the exported sample give the
same number). The rule — a FLOOR, ratcheted up as behavior yaks land, never
lowered — is written next to the config, along with why each module is excluded
(test scaffolding only: fixtures measure the tests, not the library).
Wiring, per the fast/push split: `test.coverage` runs the suite once with
`--cover --export-coverage default`; the `coverage` sensor then enforces the
floor from that export via `mix test.coverage` (no second suite run). So
`verify` runs the suite first and the sensors after it. The sensor guards on the
export existing: `mix test.coverage` exits 0 when it finds no .coverdata, and a
floor check that did not run must not report success.
blogex/.gitignore: cover/ (generated).
`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.
Records the two loops (make check per commit, make verify per push), how to
install the hook (make hooks), the deliberate --no-verify bypass, the Lens
practice (separate pass, don't chase nits inline, file a yak), the triage
classes (real bug / environment-dependent test / design nit), and how that
reconciles with the zero-defects policy. Playwright stays outside the Lens
pass pending an explicit decision.
.githooks/pre-push runs 'make verify', tees the log to .git/lens-pass.log and,
on failure, prints one summary (last step started, log path, bypass) before
exiting 1. 'make hooks' symlinks .githooks/* into the clone's hooks dir.
core.hooksPath is deliberately left alone: bd keeps its own pre-commit
(post-merge) hooks in .git/hooks and pointing hooksPath at .githooks would
silently retire them while bd still reports them installed.
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.
Root check now loops app then blogex then the pin check, mirroring the shape
the Lens pass will use. blogex gains a check target (compile with warnings
fatal + format); credo is a marked slot until its dep exists, and the gate
never claims a sensor it did not run.
Records the plan for bringing coverage floors, CRAP scores, credo strict,
sobelow, boundary and mutation testing to firehose, framed as the
"internal verification" counterpart to the existing Playwright / conn /
LiveView tests.
Ordered in phases because the fast/push split (make verify + pre-push
hook, cheap precommit) is the container the other sensors wire into.
Includes the steering principles that made the stickies run work
(behavior not state, functional core/imperative shell, delete dead code,
leave defensive branches uncovered deliberately) and the deferred
decisions (CRAP threshold below 30, boundary.spec.check, Playwright in
the Lens pass).