<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archsetup/tests, branch main</title>
<subtitle>Builds a full dev workstation from a bare Arch Linux install.
</subtitle>
<id>https://git.cjennings.net/archsetup/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/archsetup/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/'/>
<updated>2026-09-13T12:02:39+00:00</updated>
<entry>
<title>fix(install): mask passim when fwupd is installed</title>
<updated>2026-09-13T12:02:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T12:02:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=38b1758716f5e5086eac9e9cdc0f0adfba9e54e5'/>
<id>urn:sha1:38b1758716f5e5086eac9e9cdc0f0adfba9e54e5</id>
<content type='text'>
passim is fwupd's LAN metadata-sharing daemon and listens publicly on 0.0.0.0:27500. Any fwupdmgr run D-Bus-activates it. The unit is static, so disabling it is a no-op and it comes back on the next run. Masking is what holds. Every install gets it, not only laptops: the listener is public on any host, and ratio has run with it masked since July without fwupd missing it. Velox had only been disabled and got the mask on 2026-09-12.
</content>
</entry>
<entry>
<title>fix(install): stop enabling the WS-Discovery host daemon</title>
<updated>2026-09-13T12:02:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T12:02:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=43acf51c4eb036c867df2fbeef99dcaef17a09a4'/>
<id>urn:sha1:43acf51c4eb036c867df2fbeef99dcaef17a09a4</id>
<content type='text'>
wsdd.service advertises this machine as a Samba host to Windows clients. Nothing the installer sets up runs Samba, so it advertised a share server that doesn't exist while listening on every interface, VPN and tailscale links included. Browsing Windows shares is the other direction: gvfs-wsdd spawns its own wsdd in discovery mode, so the package stays and only the service goes. A re-run on a machine set up before this disables the unit. A fresh install has no unit yet and skips quietly.
</content>
</entry>
<entry>
<title>feat(install): default DNS over TLS off on the Proton tunnel links</title>
<updated>2026-09-13T12:02:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T12:02:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=2e303a07d1a6e4de85277a34669f8da61f7fdb6e'/>
<id>urn:sha1:2e303a07d1a6e4de85277a34669f8da61f7fdb6e</id>
<content type='text'>
The resolved drop-in pins DNSOverTLS=yes for every link. Proton VPN (proton0) and the static Proton WireGuard profiles (wgpvpn) push an in-tunnel resolver (10.2.0.1) that answers plain port 53 and never completes TLS on 853, so every lookup through the tunnel hung. The Proton client recreates its NetworkManager profile on each connect, so a per-profile setting can't stick.

NM pushes a [connection-tunnel-dot] default matched on those two interface names to resolved on every activation. Wifi and everything else keep the strict setting. Ratio and velox already carry the drop-in by hand. This makes a rebuild carry it too.
</content>
</entry>
<entry>
<title>feat(bluetooth): restore the radio after a sleep cycle</title>
<updated>2026-08-21T14:25:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-21T14:25:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=d1b5ee1fdc07400e4308db7d346c5a8be0309c7a'/>
<id>urn:sha1:d1b5ee1fdc07400e4308db7d346c5a8be0309c7a</id>
<content type='text'>
Masking systemd-rfkill so TLP can own radios leaves the resume edge with no owner at all. TLP's sleep hook runs `tlp resume`, but its setting is DEVICES_TO_ENABLE_ON_STARTUP, and TLP has no ON_RESUME to pair with it. WiFi survives anyway because NetworkManager unblocks itself. Bluetooth has no equivalent, so it comes back soft-blocked and stays that way.

After a hibernate the controller is wedged as well, which is why unblocking alone is not the fix. I cleared rfkill by hand on velox this morning and scanning still returned nothing. Zero devices, in a room that gave seventeen a minute after the driver was reloaded. bluetoothd had logged "Failed to set mode" and "Failed to add device" at the instant of resume.

So the hook reloads btusb on a hibernate-class wake, then unblocks. That order matters: a freshly loaded btusb can come up blocked and would undo an earlier unblock. A plain suspend only unblocks, since it brings USB back intact and reloading there would tear down a working adapter for nothing.

It acts only where TLP's own config asks for bluetooth, so it re-asserts a declared intent instead of inventing one and a machine that deliberately keeps the radio off keeps it off. It exits zero on every path, because a failing sleep hook logs noise that outlives the cause.

The install sits next to the mask that creates the gap rather than beside the other installs, and creates /etc/systemd/system-sleep first. Arch does not ship it and install_executable is a plain cp, so without the mkdir a fresh machine would warn and end up with no hook.

Ten tests, and I checked they bite: dropping the TLP intent guard fails three, moving the unblock before the reload fails one.

This surfaced because hibernate was switched back on. The gap predates that and would have shown up the first time the laptop slept.
</content>
</entry>
<entry>
<title>fix(post-rebuild-check): stop expecting a .claude directory every project names</title>
<updated>2026-08-20T16:45:58+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T16:45:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=3fbf3e022a364fe30609fb2d8dbc49ad6c961f70'/>
<id>urn:sha1:3fbf3e022a364fe30609fb2d8dbc49ad6c961f70</id>
<content type='text'>
Check 4 expects a tooling path wherever the project's own .gitignore names it, on the reasoning that the ignore file records what the project means to hold untracked. That holds for .ai and todo.org. It doesn't hold for .claude, because the bootstrap and the gitignore sweep write that line into every gitignore-mode project whether or not one ever exists there. The entry is aspirational, not a promise.

pearl, rsyncshot and yt-sync each name it and none has ever had one. Ratio is missing the identical directory in the identical three, and ratio was never rebuilt, so this is the steady state rather than drift. That is the same test CLAUDE.md is already excluded by, one comment above.

Dropping it costs no real signal. A project that genuinely carries a .claude, with rules and hooks from a language bundle, has it re-synced by sync-language-bundle.sh at every session start, so a true absence heals before this check would run.

velox now reports one finding instead of eight. Seven of the eight were this checker being wrong rather than the machine.
</content>
</entry>
<entry>
<title>feat(post-rebuild-check): let a machine declare which units it means to leave disabled</title>
<updated>2026-08-20T13:01:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T13:01:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=c588b0841df440e1a5290c7150ee245f90d1788e'/>
<id>urn:sha1:c588b0841df440e1a5290c7150ee245f90d1788e</id>
<content type='text'>
Check 2 treats "enabled" as a proxy for "will actually run", and the proxy is wrong for a unit nobody intends to enable on that box. velox has four: geoclue-agent is redundant because hyprland's exec-once starts the binary itself, emacs is started on demand by emacsclient, obs-record-watchdog only matters while recording, and obsbot-wb-guard needs an OBSBOT the laptop doesn't have. All four reported at every run.

Four permanent lines in front of every real finding teach me to skim the output, which is the argument check 4 already makes about CLAUDE.md. On velox this takes the run from 8 findings to 4.

The list is machine-local, read from $XDG_CONFIG_HOME/post-rebuild-check/units-expected-disabled. It can't live in the unit file, because obsbot-wb-guard is correctly enabled on ratio. One unit, a different right answer per machine.

An entry whose unit is enabled after all is reported too. The main loop can't catch that, since it skips any state that isn't disabled or linked, so the list gets its own pass. Without it a stale exemption sits there suppressing nothing, and the list becomes somewhere real findings go to die.

The exemption is tested last, so it never hides the dangling-symlink finding decided on the filesystem above it.
</content>
</entry>
<entry>
<title>feat(post-rebuild-check): flag a stopped idle daemon and a read-only remote</title>
<updated>2026-08-20T07:17:09+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T07:17:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=87ff0b77cdc1d8a66a063cfe471dab78d1966dc4'/>
<id>urn:sha1:87ff0b77cdc1d8a66a063cfe471dab78d1966dc4</id>
<content type='text'>
Both are states where the machine looks finished and isn't, which is the whole point of this script.

Check 7 asks whether hypridle is running. Nothing else notices when it isn't. Idle lock and suspend stop happening, and the laptop runs until its battery is gone. That's how velox reset its RTC on 2026-08-19, which is what dropped it into check 6's clock and DNS deadlock. The check asks whether the daemon is alive rather than why it might not be, so a crash and a stale caffeine surface alike. It's gated on hypridle being installed, since only Hyprland machines get it.

Check 8 asks whether the working repos can push. The installer clones them from the read-only https endpoint. That's right for someone installing archsetup with no key on my server, and wrong for my own machines. Nothing about the tree shows it. velox's dotfiles remote sat that way for four days and announced itself as a 403.

Only my own read-only endpoint is flagged. An https remote elsewhere may push fine through a credential helper, and guessing about hosts this machine doesn't own would stand noise in front of real findings.
</content>
</entry>
<entry>
<title>fix(installer): clone my working repos with full history</title>
<updated>2026-08-20T07:15:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T07:15:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=a028aa589056167160b39cf9e023c12dd30dec34'/>
<id>urn:sha1:a028aa589056167160b39cf9e023c12dd30dec34</id>
<content type='text'>
archsetup and dotfiles were cloned with --depth 1. They aren't build trees. They're the two repos I develop in on every machine this installs, and velox came back from its rebuild carrying 7 commits of history in each instead of 851.

A shallow clone degrades quietly. Past the graft point `git log -- &lt;path&gt;` answers "no commits" rather than failing, so a search of history for specific files came back empty and exited clean. The clone couldn't see the history they live in. Blame and bisect break the same way, less dangerously.

Dropping --depth also drops the implicit --single-branch, so both clones now fetch every branch. That's what I want in a repo I work in.

The AUR build clones stay shallow. They exist to run make install and are then discarded. A test asserts they keep --depth 1, so a broad sed can't take them along.
</content>
</entry>
<entry>
<title>fix(test): pin stdin so the net-scenario harness stops hanging</title>
<updated>2026-08-19T21:28:17+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-19T21:28:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=5b3b64ad66679bde7d90d5937794f4125d5a2fd2'/>
<id>urn:sha1:5b3b64ad66679bde7d90d5937794f4125d5a2fd2</id>
<content type='text'>
The stubbed ssh is `cat &gt;/dev/null`, which drains stdin to EOF. Without an explicit stdin the stub inherits whatever the test runner had, so it returned instantly when stdin was redirected and blocked forever when it was a terminal or a live pipe. All five tests then hit their 20-second timeout.

That made the suite unreliable in the case that matters most, running it by hand. `make test-unit &lt;/dev/null` passed while the same target run from a terminal failed, which reads as a flaky test rather than an environment-dependent one.
</content>
</entry>
<entry>
<title>fix(check): read NTP sources only from files chrony is told to read</title>
<updated>2026-08-19T19:32:02+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-19T19:32:02+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=ec3a63caca4f2d955e594318a9a690e4c28af19e'/>
<id>urn:sha1:ec3a63caca4f2d955e594318a9a690e4c28af19e</id>
<content type='text'>
Check 6 globbed /etc/chrony.d unconditionally. A drop-in is inert unless chrony.conf names its directory, and Arch's stock chrony.conf names none, so a machine with the IP-addressed source on disk and no confdir line would show the literal and pass. That is a false pass on exactly the misconfiguration the check exists to catch, and it describes a file chrony never opens.

Sources now come only from chrony.conf plus whatever confdir or sourcedir it actually names. The config path is a seam so the confdir logic can be tested against a fixture instead of the real /etc.

This should have been in the previous commit, whose message already describes it. I staged before reviewing, fixed the finding, then committed the stale index.
</content>
</entry>
</feed>
