starting new job...
→ running command: cd "/etc/byggsteg/job-clone//wolk-jjba/trunk" && git reset --hard && git pull
HEAD is now at 2996e14 chore: Update channels to the latest Guix master
From https://codeberg.org/jjba23/wolk-jjba
2996e14..200a25c trunk -> origin/trunk
Updating 2996e14..200a25c
Fast-forward
maak.scm | 34 ++++++++++++++++++++++++++++-
test/veritas/unit/operating-system-spec.scm | 15 ++++++++++++-
2 files changed, 47 insertions(+), 2 deletions(-)
→ running command: cd /etc/byggsteg/job-clone/wolk-jjba/trunk && uname -a
Linux wolk-jjba 7.2.11-gnu #1 SMP PREEMPT_DYNAMIC 1 x86_64 GNU/Linux
→ running command: cd /etc/byggsteg/job-clone/wolk-jjba/trunk && git log --name-status HEAD^..HEAD
commit 200a25ce1ce05b2ef45dd6bbe40bdce8a2d2e728
Author: Josep Bigorra <jjbigorra@gmail.com>
Date: Mon Oct 26 17:46:51 2026 +0100
fix: Improve test setup
M maak.scm
M test/veritas/unit/operating-system-spec.scm
→ running command: cd /etc/byggsteg/job-clone/wolk-jjba/trunk && maak test
channels.scm:47:6: warning: channel 'jointhefreeworld' is not trusted
The following derivation will be built:
/gnu/store/cimqss20yiv7wrj00x4h92cvdz0li14a-profile.drv
building CA certificate bundle...
listing Emacs sub-directories...
building fonts directory...
building directory of Info manuals...
building profile with 8 packages...
scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 194873666 (replay with VERITAS_SEED=194873666)
found unit tests: (gfs-spec.scm secrets-spec.scm jobs-spec.scm maintenance-spec.scm operating-system-spec.scm nginx-spec.scm radio-spec.scm iptables-spec.scm bash-spec.scm containers-spec.scm temp-spec.scm sysctl-spec.scm certbot-spec.scm prelude-spec.scm logging-spec.scm)
running unit tests from file: gfs-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] GFS Retention Suite
[🧪 test] Past a month, the month's representative survives
[⚙️ assert] No month is represented twice
✅ got: 7
[⚙️ assert] A deleted archive's month is kept by another
✅ got: #t
[⚙️ assert] Those survivors are grandfather archives
✅ got: #t
[🧪 test] Every archive up to a week old survives
[⚙️ assert] Ages 0 to 7 are all kept
✅ got: (0 1 2 3 4 5 6 7)
[⚙️ assert] Nothing in the son window is deleted
✅ got: ()
[⚙️ assert] All of them are kept as son
✅ got: #t
[🧪 test] A dry run reports without touching the box
[⚙️ assert] No copy in a dry run
✅ got: ()
[⚙️ assert] No deletion in a dry run
✅ got: ()
[🧪 test] Every archive is kept or deleted, never both
[⚙️ assert] The two lists partition the directory
✅ got: 40
[⚙️ assert] No archive appears in both
✅ got: ()
[⚙️ assert] Nothing kept is over a year old
✅ got: #t
[🧪 test] Promotion is once per week and once per month
[⚙️ assert] An empty archive tree takes the newest
✅ got: ((1792584000 . proj-backup-1792584000.tar.gz.enc))
[⚙️ assert] A week that is already represented needs no copy
✅ got: ()
[⚙️ assert] A month that is already represented still takes a new week
✅ got: ((1792584000 . proj-backup-1792584000.tar.gz.enc))
[🧪 test] Nothing older than a year survives, and a year is honoured
[⚙️ assert] 364 days old is still kept
✅ got: (364)
[⚙️ assert] 365 days and older are deleted (same month)
✅ got: (365 366 400)
[⚙️ assert] A year-old archive in another month is kept
✅ got: (300)
[🧪 test] A run promotes, prunes and reports
[⚙️ assert] One long-term archive per project
✅ got: ((proj . 1))
[⚙️ assert] The newest mirror archive was promoted
✅ got: #t
[⚙️ assert] A stray file in the archive tree survives
✅ got: #t
[⚙️ assert] The expired archive was deleted
✅ got: #f
[⚙️ assert] Nothing outside the archive tree is deleted
✅ got: #t
[⚙️ assert] The promoted archive is not then deleted
✅ got: #t
[🧪 test] An unreachable box is a failure, not an empty directory
[⚙️ assert] No project is reported as counted
✅ got: ()
[⚙️ assert] Every unreachable project is reported
✅ got: (proj other)
[⚙️ assert] A silent empty directory would leave this empty
✅ got: #t
[🧪 test] Running a command returns its lines and its exit status
[⚙️ assert] The output is read line by line
✅ got: (one two)
[⚙️ assert] The exit status is not swallowed
✅ got: 3
[🧪 test] Shell quoting survives an apostrophe
[⚙️ assert] A quote is escaped the POSIX way
✅ got: 'it'\''s'
[⚙️ assert] Spaces stay inside one word
✅ got: 'ls -1 /a b'
[🧪 test] A run twice in a row changes nothing the second time
[⚙️ assert] The archive tree did not grow
✅ got: 2
[⚙️ assert] The second run copied nothing
✅ got: 2
[🧪 test] Past a week, the week's representative survives
[⚙️ assert] No week is represented twice
✅ got: 5
[⚙️ assert] A deleted archive's week is kept by another
✅ got: #t
[⚙️ assert] Those survivors are father archives
✅ got: #t
[🧪 test] Archive names identify an archive; sidecars and strays do not
[⚙️ assert] A tar archive carries its timestamp
✅ got: 1700000000
[⚙️ assert] So does an SQL dump
✅ got: 1700000000
[⚙️ assert] The checksum sidecar is not an archive
✅ got: #f
[⚙️ assert] A stray file is not an archive
✅ got: #f
[⚙️ assert] An unencrypted tarball is not an archive
✅ got: #f
[🧪 test] Parsing sorts newest first and drops everything else
[⚙️ assert] Newest first, strays ignored
✅ got: (300 200 100)
[⚙️ assert] An empty directory parses to nothing
✅ got: ()
ℹ️ info - ran 1 tasks in: 0 seconds, 26898 microseconds
running unit tests from file: secrets-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Secrets Suite
[🧪 test] A token is read from the first line, trimmed
[⚙️ assert] Surrounding whitespace is dropped
✅ got: value-with-space
[⚙️ assert] An empty secret has no value
✅ got: #f
[⚙️ assert] Neither does one that is only whitespace
✅ got: #f
[⚙️ assert] A missing secret is #f, not an error
✅ got: #f
[🧪 test] A missing secret is not an error
[⚙️ assert] The registry is reported as missing
✅ got: missing
[⚙️ assert] and no file is claimed for it
✅ got: ()
[⚙️ assert] Nothing is written for a secret that is not there
✅ got: #t
[🧪 test] A changed token does rewrite, atomically
[⚙️ assert] A new token is written
✅ got: written
[⚙️ assert] and the file holds the new credential
✅ got: {"auths": {"docker.io": {"auth": "ampiYTIzOmEtbmV3LXRva2Vu"}}}
[⚙️ assert] No temporary file is left behind
✅ got: #t
[🧪 test] A dry run writes nothing
[⚙️ assert] The dry run says what it would write
✅ got: would-write
[⚙️ assert] and writes nothing
✅ got: #t
[🧪 test] The timer is scheduled, logged and rotated
[⚙️ assert] The capability provides a secrets-apply service
✅ got: #t
[⚙️ assert] It runs every quarter of an hour
✅ got: #t
[⚙️ assert] It runs the Scheme program, not a shell script
✅ got: #t
[⚙️ assert] Its log file is rotated with the rest
✅ got: #t
[⚙️ assert] And it is respawned
✅ got: #t
[🧪 test] The base64 encoder matches RFC 4648
[⚙️ assert] One byte is padded with two =
✅ got: Zg==
[⚙️ assert] Two bytes are padded with one =
✅ got: Zm8=
[⚙️ assert] Three bytes need no padding
✅ got: Zm9v
[⚙️ assert] Four bytes carry into the next group
✅ got: Zm9vYg==
[⚙️ assert] The full vector
✅ got: Zm9vYmFy
[⚙️ assert] Nothing encodes to nothing
✅ got:
[⚙️ assert] A non-ASCII character is encoded as its UTF-8 bytes
✅ got: w6k=
[🧪 test] A credential that would need JSON escaping is refused
[⚙️ assert] A quote in the username is an error
✅ got: #t
[⚙️ assert] So is a backslash
✅ got: #t
[🧪 test] apply-secrets writes one 0600 auth.json per account
[⚙️ assert] The report names the registry
✅ got: docker.io
[⚙️ assert] And says the credential was applied
✅ got: applied
[⚙️ assert] joe has an auth file
✅ got: #t
[⚙️ assert] so does byggsteg-runner
✅ got: #t
[⚙️ assert] The auth file is readable by its owner only
✅ got: 384
[⚙️ assert] and so is the directory it lives in
✅ got: 448
[⚙️ assert] The file is the credential podman reads
✅ got: {"auths": {"docker.io": {"auth": "ampiYTIzOmRja3JfcGF0X1NQRUNUT0tFTg=="}}}
[🧪 test] The program logs in every account that builds containers
[⚙️ assert] joe is passed to secrets-apply
✅ got: #t
[⚙️ assert] byggsteg-runner is passed to secrets-apply
✅ got: #t
[⚙️ assert] The timer applies; the dry run is for humans
✅ got: #t
[⚙️ assert] The program imports only this repository's module
✅ got: #t
[🧪 test] A second run rewrites nothing
[⚙️ assert] The file is reported as unchanged
✅ got: unchanged
[⚙️ assert] The file is not replaced, not even by a rename
✅ got: 11219360
[⚙️ assert] And its contents are untouched
✅ got: {"auths": {"docker.io": {"auth": "ampiYTIzOmRja3JfcGF0X1NQRUNUT0tFTg=="}}}
[🧪 test] auth.json is the file podman reads
[⚙️ assert] One registry, one auth entry, base64 of user:token
✅ got: {"auths": {"docker.io": {"auth": "ampiYTIzOmRja3JfcGF0X1NQRUNUT0tFTg=="}}}
[⚙️ assert] The top-level key is auths
✅ got: #t
[⚙️ assert] The token itself never appears in the file
✅ got: #t
[🧪 test] An account that does not exist is skipped, the rest are written
[⚙️ assert] joe still gets a file
✅ got: #t
[⚙️ assert] The unknown account is reported as such
✅ got: no-such-user
ℹ️ info - ran 1 tasks in: 0 seconds, 15132 microseconds
running unit tests from file: jobs-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Jobs Suite
[🧪 test] The weekly upgrade stays outside the Shepherd
[⚙️ assert] The reconfigure job extends mcron, not shepherd
✅ got: (mcron)
[🧪 test] The schedules are the ones the README documents
[⚙️ assert] wikimusic is archived twice a day
✅ got: #t
[⚙️ assert] Minecraft is pruned hourly
✅ got: #t
[⚙️ assert] PostgreSQL is dumped twice a day
✅ got: #t
[⚙️ assert] The mirror runs just before the hour
✅ got: #t
[🧪 test] A dump backup refuses an empty dump
[⚙️ assert] The command writes the archive
✅ got: #t
[⚙️ assert] An empty dump fails the job instead of being stored
✅ got: #t
[⚙️ assert] The dump keeps its own extension
✅ got: #t
[🧪 test] PostgreSQL is dumped, not tarred, and as the postgres role
[⚙️ assert] The dump is restorable into a live cluster
✅ got: #t
[⚙️ assert] peer authentication is satisfied by running as postgres
✅ got: #t
[🧪 test] A file backup still tars what it is given
[⚙️ assert] The include is tarred as it is
✅ got: #t
[⚙️ assert] No database is snapshotted for a file backup
✅ got: #t
[🧪 test] Applications are restarted hourly
[⚙️ assert] lucidplan is restarted by the timer
✅ got: #t
[⚙️ assert] byggsteg is restarted by the timer
✅ got: #t
[⚙️ assert] torikomi is restarted by the timer
✅ got: #t
[⚙️ assert] symbanta is restarted by the timer
✅ got: #t
[🧪 test] A database backup snapshots instead of copying live files
[⚙️ assert] The database is snapshotted with sqlite3 .backup
✅ got: #t
[⚙️ assert] The snapshot is checked before it is stored
✅ got: #t
[⚙️ assert] The archive holds the snapshot, not the live file
✅ got: #t
[⚙️ assert] The -wal and -shm files are no longer tarred
✅ got: #t
[🧪 test] Each job logs to its own file, which is rotated
[⚙️ assert] The backup log is named after the job
✅ got: #t
[⚙️ assert] Every job log is registered for rotation
✅ got: 18
[⚙️ assert] The Storage Box mirror log is rotated too
✅ got: #t
[⚙️ assert] The PostgreSQL dump log is rotated too
✅ got: #t
[🧪 test] Pruning deletes exactly the archives it should
[⚙️ assert] Only regular files matching the archive pattern
✅ got: #t
[⚙️ assert] Keeping three means dropping from the fourth
✅ got: #t
[⚙️ assert] Keeping one means dropping from the second
✅ got: #t
[⚙️ assert] The checksum goes with its archive
✅ got: #t
[⚙️ assert] The prune no longer deletes whatever sorts last
✅ got: #t
[⚙️ assert] Leftovers of an interrupted run are removed later
✅ got: #t
[🧪 test] The mirror is a delete-mirror run as an unprivileged user
[⚙️ assert] Deleted backups disappear from the Storage Box
✅ got: #t
[⚙️ assert] The Storage Box is the destination
✅ got: #t
[⚙️ assert] The mirror does not run as root
✅ got: #t
[⚙️ assert] The long-term archive is not mirrored over
✅ got: #t
[🧪 test] Pruning can also bound an archive by age
[⚙️ assert] An archive older than the age limit goes
✅ got: #t
[⚙️ assert] No age limit means count only
✅ got: #t
[🧪 test] A database backup is one transaction
[⚙️ assert] A failing step fails the job
✅ got: #t
[⚙️ assert] The archive is made in the project directory
✅ got: #t
[⚙️ assert] The archive name carries a timestamp
✅ got: #t
[⚙️ assert] A trap removes the plaintext and partial files
✅ got: #t
[⚙️ assert] The trap names the archive it cleans up
✅ got: #t
[⚙️ assert] The archive is encrypted on the way out
✅ got: #t
[⚙️ assert] The passphrase is read from a file, not the script
✅ got: #t
[⚙️ assert] The ciphertext is staged and renamed into place
✅ got: #t
[⚙️ assert] A checksum sidecar is written next to the archive
✅ got: #t
[⚙️ assert] The archive and its checksum are moved into place
✅ got: #t
[⚙️ assert] The destination is created if missing
✅ got: #t
[🧪 test] Every job is a timer with a log and a trigger
[⚙️ assert] Eighteen jobs are scheduled
✅ got: 18
[⚙️ assert] No job is scheduled twice
✅ got: 18
[⚙️ assert] Every timer is respawned
✅ got: #t
[⚙️ assert] A job can be run by hand with herd trigger
✅ got: (trigger)
[⚙️ assert] The timers come from the Shepherd's timer module
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 18523 microseconds
running unit tests from file: maintenance-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Maintenance Suite
[🧪 test] The Guix collection frees generations before the store
[⚙️ assert] Old system generations go first
✅ got: #t
[⚙️ assert] joe's home generations go too
✅ got: #t
[⚙️ assert] The home profile is pruned as its owner
✅ got: #t
[⚙️ assert] The store is collected afterwards
✅ got: #t
[⚙️ assert] The size of the store is published
✅ got: #t
[🧪 test] The long-term retention job runs the Scheme program, unprivileged
[⚙️ assert] It runs every night at 02:45
✅ got: #t
[⚙️ assert] It runs the gfs-backup program, not a shell script
✅ got: #t
[⚙️ assert] It runs as the user that owns the Storage Box key
✅ got: #t
[⚙️ assert] With that user's HOME, so ssh finds the alias
✅ got: #t
[⚙️ assert] No shell is involved
✅ got: #t
[🧪 test] The temporary-file cleanup spares sockets
[⚙️ assert] A long-running daemon keeps its socket
✅ got: #t
[⚙️ assert] Only week-old files are candidates
✅ got: #t
[⚙️ assert] Both temporary directories are covered
✅ got: #t
[🧪 test] The disk report publishes what the alerts need
[⚙️ assert] One series per project, with a project label
✅ got: #t
[⚙️ assert] The newest archive of each project is dated
✅ got: #t
[⚙️ assert] A project without archives reports 0, not nothing
✅ got: #t
[⚙️ assert] The report dates itself, so a stale report is visible
✅ got: #t
[⚙️ assert] The backup tree is the thing being reported
✅ got: #t
[🧪 test] Pruning podman state leaves volumes alone
[⚙️ assert] Unused containers, images and build cache go
✅ got: #t
[⚙️ assert] Only things untouched for a week
✅ got: #t
[⚙️ assert] Volumes are never pruned: a container may own data there
✅ got: #t
[⚙️ assert] The CI runner's images are pruned too
✅ got: #t
[🧪 test] The disk report publishes the long-term retention state
[⚙️ assert] The last successful GFS run is a metric
✅ got: #t
[⚙️ assert] And so is the archive count per project
✅ got: #t
[⚙️ assert] It reads the marker the job leaves behind
✅ got: #t
[🧪 test] The backup verification actually reads the archive
[⚙️ assert] The checksum sidecar is checked
✅ got: #t
[⚙️ assert] A tar archive is listed through a pipe
✅ got: #t
[⚙️ assert] An SQL dump is tested through a pipe
✅ got: #t
[⚙️ assert] A failed verification fails the job
✅ got: #t
[⚙️ assert] A success is published, so a stale check is visible
✅ got: #t
[⚙️ assert] Nothing is decrypted to a temporary file
✅ got: #t
[🧪 test] The caches that are deleted are download caches
[⚙️ assert] The npm cache goes
✅ got: #t
[⚙️ assert] The pip cache goes
✅ got: #t
[⚙️ assert] The cache is removed, not truncated
✅ got: #t
[🧪 test] Textfiles are replaced atomically
[⚙️ assert] The new file is written under a temporary name
✅ got: #t
[⚙️ assert] The temporary file is renamed into place
✅ got: #t
[⚙️ assert] It lands in node exporter's textfile directory
✅ got: #t
[⚙️ assert] And never as a partial .prom file
✅ got: #t
[🧪 test] The service creates the metrics directory it writes into
[⚙️ assert] There is an activation extension
✅ got: #t
[⚙️ assert] The activation creates node exporter's textfile directory
✅ got: #t
[🧪 test] Every maintenance job is scheduled exactly once
[⚙️ assert] The eight jobs are the documented ones
✅ got: (disk-report guix-gc nix-gc verify-backups gfs-backup podman-prune cache-clean tmp-clean)
[⚙️ assert] No job is scheduled twice
✅ got: 8
[⚙️ assert] Every job is respawned
✅ got: #tWARNING: (jointhefreeworld packages applications): `guile-scriba' imported from both (gnu packages guile-xyz) and (jointhefreeworld packages guile)
WARNING: (jointhefreeworld packages applications): `guile-scriba' imported from both (jointhefreeworld packages guile) and (gnu packages guile-xyz)
WARNING: (jointhefreeworld packages applications): `guile-scriba' imported from both (gnu packages guile-xyz) and (jointhefreeworld packages guile)
WARNING: (jointhefreeworld packages applications): `guile-scriba' imported from both (jointhefreeworld packages guile) and (gnu packages guile-xyz)
Backtrace:
2 (primitive-load "/etc/byggsteg/job-clone/wolk-jjba/trunk/config.scm")
In ice-9/eval.scm:
626:19 1 (_ #<directory (guile-user) 7f8dbaf18c80>)
In ice-9/boot-9.scm:
2058:7 0 (error _ . _)
ice-9/boot-9.scm:2058:7: In procedure error:
missing secret: /tmp/wolk-jjba-spec-28073-1793033396-8/no-such-directory/mpd-admin
The radio reads four secrets from /tmp/wolk-jjba-spec-28073-1793033396-8/no-such-directory: mpd-admin, mpd-read, icecast-source and icecast-admin.
Create them as described in "Create the secrets" in README.org, e.g.
openssl rand -base64 24 | sudo tee /tmp/wolk-jjba-spec-28073-1793033396-8/no-such-directory/mpd-admin >/dev/null
sudo chmod 600 /tmp/wolk-jjba-spec-28073-1793033396-8/no-such-directory/mpd-admin
or push them from the machine you keep them on with `maak secrets'.
[🧪 test] Every job logs to a rotated file
[⚙️ assert] The log files are registered for rotation
✅ got: (/var/log/wolk-jjba/disk-report.log /var/log/wolk-jjba/guix-gc.log /var/log/wolk-jjba/nix-gc.log /var/log/wolk-jjba/verify-backups.log /var/log/wolk-jjba/gfs-backup.log /var/log/wolk-jjba/podman-prune.log /var/log/wolk-jjba/cache-clean.log /var/log/wolk-jjba/tmp-clean.log)
[⚙️ assert] The garbage collection log is kept
✅ got: #t
ℹ️ info - ran 1 tasks in: 1 seconds, -900789 microseconds
running unit tests from file: operating-system-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Operating System Suite
[🧪 test] The machine still has its services
[⚙️ assert] The service list is the whole machine
✅ got: #t
[🧪 test] The languages a visitor may ask for are compiled
[⚙️ assert] Japanese, Greek, Chinese (both scripts), Arabic and Russian
✅ got: (#t #t #t #t #t #t)
[🧪 test] The passwords come from the secrets directory the environment names
[⚙️ assert] The admin password is the fixture's
✅ got: fixture-admin-password
[⚙️ assert] The read password is the fixture's
✅ got: fixture-read-password
[🧪 test] The system configuration evaluates as an ordinary user
[⚙️ assert] config.scm evaluates without root
✅ got: 0
[⚙️ assert] The host keeps its name
✅ got: wolk-jjba
[⚙️ assert] The result is an operating-system
✅ got: #t
[🧪 test] A missing secrets directory fails the evaluation
[⚙️ assert] config.scm cannot silently ship without secrets
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 2741 microseconds
running unit tests from file: nginx-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Nginx Suite
[🧪 test] Minimal mode keeps only the ACME and metrics vhosts
[⚙️ assert] A new machine gets two blocks
✅ got: 2
[⚙️ assert] A configured machine serves seventeen
✅ got: 17
[🧪 test] A reverse proxy passes the client's view upstream
[⚙️ assert] The domain defaults to jointhefreeworld.org
✅ got: (probe.jointhefreeworld.org www.probe.jointhefreeworld.org)
[⚙️ assert] The vhost proxies to its own upstream
✅ got: #t
[⚙️ assert] The upstream learns the client's scheme
✅ got: #t
[⚙️ assert] WebSocket upgrades survive the proxy
✅ got: #t
[⚙️ assert] Static assets are cached upstream too
✅ got: #t
[⚙️ assert] with-gzip? #f disables upstream gzip
✅ got: #t
[⚙️ assert] with-cors? adds the CORS header
✅ got: #t
[🧪 test] The ACME vhost answers on plain HTTP and redirects
[⚙️ assert] The ACME vhost listens on port 80
✅ got: #t
[⚙️ assert] Minimal mode emits no TLS vhost
✅ got: #f
[⚙️ assert] Plain HTTP is redirected to HTTPS
✅ got: #t
[⚙️ assert] The ACME block answers for the Grafana name
✅ got: #t
[🧪 test] Restricted vhosts carry basic authentication
[⚙️ assert] torikomi is password protected
✅ got: #t
[🧪 test] The static sites serve from /srv/http
[⚙️ assert] A static site is rooted at its own name
✅ got: /srv/http/jointhefreeworld.org
[⚙️ assert] A site may point somewhere else
✅ got: /srv/symbanta.jointhefreeworld.org
[🧪 test] The ACME vhost on port 80 is rate limited
[⚙️ assert] Plain HTTP gets a request bucket
✅ got: #t
[⚙️ assert] and a connection cap
✅ got: #t
[🧪 test] The global block carries the tuning and the zones
[⚙️ assert] sendfile is on
✅ got: #t
[⚙️ assert] The proxy rate-limit zone exists
✅ got: #t
[⚙️ assert] The static rate-limit zone exists
✅ got: #t
[⚙️ assert] The WebSocket map is defined
✅ got: #t
[⚙️ assert] The nginx version is hidden
✅ got: #t
[🧪 test] A static vhost applies the shared security baseline
[⚙️ assert] HSTS is sent
✅ got: #t
[⚙️ assert] A CSP is sent
✅ got: #t
[⚙️ assert] nosniff is sent
✅ got: #t
[⚙️ assert] HTTP/2 is enabled
✅ got: #t
[⚙️ assert] Per-IP connections are capped
✅ got: #t
[⚙️ assert] with-listdir? turns the index on
✅ got: #t
[⚙️ assert] with-rate-limit? #f removes the request cap
✅ got: #f
[🧪 test] Every upstream a vhost names is declared
[⚙️ assert] Ten upstreams are declared
✅ got: 10
[⚙️ assert] Each proxy_pass names a declared upstream
✅ got: ()
[🧪 test] The administrative vhosts are locked down
[⚙️ assert] Grafana uses the strict request bucket
✅ got: #t
[⚙️ assert] Prometheus uses the strict request bucket
✅ got: #t
[⚙️ assert] The node exporter uses the strict request bucket
✅ got: #t
[⚙️ assert] Grafana caps connections per address
✅ got: #t
[⚙️ assert] and not with the application bucket
✅ got: #f
[⚙️ assert] The node exporter is no longer public
✅ got: #t
[⚙️ assert] Grafana is not behind basic auth as well as its own login
✅ got: #f
[⚙️ assert] Prometheus keeps its password
✅ got: #t
[⚙️ assert] An admin vhost takes no large uploads
✅ got: #t
[🧪 test] A proxied static asset keeps the client's Host
[⚙️ assert] The server block sends the client's Host
✅ got: #t
[⚙️ assert] Upstream keepalive still speaks HTTP/1.1
✅ got: #t
[⚙️ assert] The asset location declares none, so it inherits
✅ got: #f
[🧪 test] Every vhost denies dotfiles
[⚙️ assert] The dotfile rule is the first location
✅ got: ~ /\.(?!well-known)
[⚙️ assert] and it denies everything
✅ got: (deny all;)
[🧪 test] The access log is nginx's logfmt format
[⚙️ assert] The capability selects its own log format
✅ got: wolk
[⚙️ assert] Request time is logged
✅ got: #t
[⚙️ assert] Upstream response time is logged
✅ got: #t
[⚙️ assert] The status code is logged
✅ got: #t
[🧪 test] A static vhost serves its own directory
[⚙️ assert] with-www? #f leaves the bare name alone
✅ got: (example.test)
[⚙️ assert] The root is the site directory under /srv/http
✅ got: /srv/http/example.test
[⚙️ assert] The certificate follows the name
✅ got: /etc/letsencrypt/live/example.test/fullchain.pem
[⚙️ assert] TLS and QUIC are both served
✅ got: (443 ssl 443 quic)
[🧪 test] Every vhost has a server block
[⚙️ assert] The count is the one the README documents
✅ got: 17
[⚙️ assert] jointhefreeworld.org is served
✅ got: #t
[⚙️ assert] byggsteg is served
✅ got: #t
[⚙️ assert] torikomi is served
✅ got: #t
[⚙️ assert] symbanta is served
✅ got: #t
[🧪 test] Upstreams are on loopback with keepalive
[⚙️ assert] No upstream leaves the machine
✅ got: #t
[⚙️ assert] Every request/response upstream keeps connections alive
✅ got: #t
[⚙️ assert] The streaming upstream is not pooled
✅ got: #t
[⚙️ assert] byggsteg listens where its daemon does
✅ got: (127.0.0.1:50002)
ℹ️ info - ran 1 tasks in: 0 seconds, 14190 microseconds
running unit tests from file: radio-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Radio Suite
[🧪 test] nginx proxies the stream without buffering it
[⚙️ assert] A ^~ location wins over the static-asset regex
✅ got: ^~ /radio/
[⚙️ assert] Audio is forwarded, not buffered
✅ got: #t
[⚙️ assert] A stream is one request that never ends
✅ got: #t
[⚙️ assert] The upstream speaks HTTP/1.1
✅ got: #t
[⚙️ assert] Web players on another origin are allowed
✅ got: #t
[⚙️ assert] The stream is not cacheable
✅ got: #t
[⚙️ assert] The location repeats the security headers
✅ got: #t
[🧪 test] MPD keeps its index on local disk, not on the mount
[⚙️ assert] The database is local
✅ got: /var/lib/mpd/tag_cache
[⚙️ assert] So is the state file that survives a restart
✅ got: /var/lib/mpd/state
[⚙️ assert] And the playlist directory
✅ got: /var/lib/mpd/playlists
[⚙️ assert] The library is the CIFS mount
✅ got: #t
[⚙️ assert] Eight megabytes of decode buffer absorb slow reads
✅ got: 8192
[🧪 test] Passwords are escaped before they reach the XML
[⚙️ assert] All five metacharacters are escaped
✅ got: #t
[⚙️ assert] Nothing raw is left behind
✅ got: #t
[🧪 test] No stream password is committed to the repository
[⚙️ assert] The old literal passwords are gone
✅ got: #t
[⚙️ assert] And the secret file is used instead
✅ got: #t
[🧪 test] The queue this job builds is the only thing it starts
[⚙️ assert] The port is spliced into the script once
✅ got: #t
[⚙️ assert] An empty queue is filled from the library
✅ got: #t
[⚙️ assert] And looped, so the station cannot run out
✅ got: #t
[⚙️ assert] And started at the top of the queue
✅ got: #t
[⚙️ assert] The repair says what it did, in the job log
✅ got: #t
[⚙️ assert] No bare play: a pause would be resumed by it
✅ got: #f
[⚙️ assert] A play is not attempted while MPD is down
✅ got: #t
[⚙️ assert] Nor while the library is unmounted
✅ got: #t
[⚙️ assert] Missing sources only count for a player that was already playing
✅ got: #t
[⚙️ assert] A play that did not take still counts, so a wedged player is restarted
✅ got: #t
[⚙️ assert] The start runs before the failure counter
✅ got: #t
[🧪 test] The service provides the Icecast account
[⚙️ assert] The unprivileged user is part of the capability
✅ got: #t
[🧪 test] The health job only intervenes after repeated failures
[⚙️ assert] Failures are counted across runs
✅ got: #t
[⚙️ assert] Three bad checks in a row are needed
✅ got: #t
[⚙️ assert] Icecast is restarted first
✅ got: #t
[⚙️ assert] Then MPD, which would otherwise reconnect to nothing
✅ got: #t
[⚙️ assert] MPD is only restarted when it is down or was playing
✅ got: #t
[⚙️ assert] A paused player is never bounced back into play
✅ got: #t
[🧪 test] The scan job skips a library that is not there
[⚙️ assert] It asks whether the mount is up
✅ got: #t
[⚙️ assert] And says so instead of scanning
✅ got: #t
[⚙️ assert] Otherwise it refreshes the database
✅ got: #t
[⚙️ assert] It authenticates like every other mpc call
✅ got: #t
[🧪 test] The activation creates what the Shepherd does not
[⚙️ assert] MPD's playlist directory
✅ got: #t
[⚙️ assert] The failure counter's directory
✅ got: #t
[🧪 test] Both timers are scheduled and rotated
[⚙️ assert] Two radio timers
✅ got: (radio-health mpd-scan)
[⚙️ assert] The health check runs every five minutes
✅ got: #t
[⚙️ assert] The library scan runs nightly
✅ got: #t
[⚙️ assert] Icecast's access log is rotated
✅ got: #t
[⚙️ assert] Its error log is rotated too
✅ got: #t
[⚙️ assert] And the job log
✅ got: #t
[🧪 test] Icecast is configured from the secrets, at the sizes a phone needs
[⚙️ assert] The source password comes from the secret
✅ got: #t
[⚙️ assert] And so does the admin password
✅ got: #t
[⚙️ assert] The burst is 128 KiB, not Icecast's 64 KiB default
✅ got: #t
[⚙️ assert] The queue holds two bursts
✅ got: #t
[⚙️ assert] There is room for every mount plus one
✅ got: #t
[⚙️ assert] It listens on 6690
✅ got: #t
[⚙️ assert] / is the status page
✅ got: #t
[⚙️ assert] The web root is a placeholder for the store path
✅ got: #t
[🧪 test] MPD pushes the same audio into three mounts
[⚙️ assert] Three streams
✅ got: 3
[⚙️ assert] The mounts are the documented ones
✅ got: (/radio.opus /radio-low.opus /radio.mp3)
[⚙️ assert] Two Opus and one MP3 for reach
✅ got: (opus opus lame)
[⚙️ assert] 128k, 64k for mobile data, 96k MP3
✅ got: (128000 64000 96000)
[⚙️ assert] Every output stays connected between tracks
✅ got: #t
[⚙️ assert] Every output is an Icecast client
✅ got: #t
[⚙️ assert] Every output authenticates with the source password
✅ got: #t
[🧪 test] A paused player is left alone, and cannot add up to a restart
[⚙️ assert] The two states a listener can ask for are handled together
✅ got: #t
[⚙️ assert] Both are left as they are, and the log says so
✅ got: #t
[⚙️ assert] The queue length is what separates them from an empty queue
✅ got: #t
[⚙️ assert] Only a queue with nothing in it is started
✅ got: #t
[⚙️ assert] There is exactly one way to start playback
✅ got: 1
[⚙️ assert] A paused player is never played again
✅ got: #f
[🧪 test] MPD clients must authenticate, and the two passwords differ
[⚙️ assert] An admin and a read-only password
✅ got: (admin-pw@read,add,control,admin read-pw@read,add,control)
[🧪 test] The health job authenticates and watches every mount
[⚙️ assert] The main mount is checked
✅ got: #t
[⚙️ assert] The mobile mount is checked
✅ got: #t
[⚙️ assert] The MP3 mount is checked
✅ got: #t
[⚙️ assert] MPD liveness is a TCP connect
✅ got: #t
[⚙️ assert] The library mount is checked
✅ got: #t
[⚙️ assert] The player state comes from mpc's format string
✅ got: #t
[⚙️ assert] mpc authenticates with the password from the secret
✅ got: #t
[⚙️ assert] The secrets directory is the real one
✅ got: #t
[⚙️ assert] The password is read at run time
✅ got: #t
[⚙️ assert] No password is baked into the job script
✅ got: #t
[⚙️ assert] The song count is read, for the empty-database case
✅ got: #t
[⚙️ assert] An empty database is rescanned, with its output off stdout
✅ got: #t
[🧪 test] The live vhost carries the stream
[⚙️ assert] https://jointhefreeworld.org/radio/ is served
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 43390 microseconds
running unit tests from file: iptables-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Firewall Suite
[🧪 test] The rules are a default-deny set
[⚙️ assert] The loopback is trusted
✅ got: #t
[⚙️ assert] Replies to outbound connections are allowed
✅ got: #t
[⚙️ assert] Everything else is rejected
✅ got: #t
[⚙️ assert] IPv6 rejects with the IPv6 ICMP type
✅ got: #t
[🧪 test] Both rule sets exist and are balanced
[⚙️ assert] The IPv4 rule set is not empty
✅ got: #t
[⚙️ assert] The IPv6 rule set is not empty
✅ got: #t
[⚙️ assert] IPv4 commits exactly once
✅ got: 1
[⚙️ assert] IPv6 commits exactly once
✅ got: 1
[🧪 test] Hostile traffic is bounded before it reaches a service
[⚙️ assert] Malformed packets are dropped, not parsed
✅ got: #t
[⚙️ assert] New SSH connections are rate limited per source
✅ got: #t
[⚙️ assert] Every served port has a per-source connection cap
✅ got: #t
[⚙️ assert] ICMP echo is rate limited before it is accepted
✅ got: #t
[⚙️ assert] and the excess is dropped, not queued
✅ got: #t
[⚙️ assert] IPv6 drops malformed packets too
✅ got: #t
[⚙️ assert] IPv6 caps connections per source
✅ got: #t
[🧪 test] The served ports are open
[⚙️ assert] SSH is on 2323
✅ got: #t
[⚙️ assert] HTTP is open for ACME challenges
✅ got: #t
[⚙️ assert] HTTPS is open
✅ got: #t
[⚙️ assert] HTTP/3 is open
✅ got: #t
[⚙️ assert] Icecast is open
✅ got: #t
[⚙️ assert] Icecast sees UDP sources
✅ got: #t
[⚙️ assert] Minecraft is open
✅ got: #t
[⚙️ assert] Minecraft sees UDP
✅ got: #t
[🧪 test] IPv6 mirrors IPv4
[⚙️ assert] IPv6 serves HTTPS
✅ got: #t
[⚙️ assert] IPv6 serves SSH
✅ got: #t
[⚙️ assert] IPv6 accepts ICMPv6
✅ got: #t
[⚙️ assert] IPv6 does not expose PostgreSQL
✅ got: #f
[🧪 test] Private services stay closed
[⚙️ assert] PostgreSQL is not exposed
✅ got: #f
[⚙️ assert] Grafana is reached through nginx only
✅ got: #f
[⚙️ assert] Prometheus is reached through nginx only
✅ got: #f
[⚙️ assert] The lucidplan upstream is not exposed
✅ got: #f
ℹ️ info - ran 1 tasks in: 0 seconds, 113552 microseconds
running unit tests from file: bash-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Bash Configuration Suite
[🧪 test] Server helpers wrap the checkout and the Shepherd
[⚙️ assert] The checkout path is assigned once
✅ got: 1
[⚙️ assert] fr pulls the checkout, then runs maak fr
✅ got: #t
[⚙️ assert] ht triggers a daemon and tails its log
✅ got: #t
[⚙️ assert] The machine alias uses ls, not gls
✅ got: #t
[🧪 test] Every shared alias reaches the rc
[⚙️ assert] Each shared alias is emitted verbatim
✅ got: ()
[⚙️ assert] hg still points at hgrep
✅ got: #t
[⚙️ assert] the multi-character alias keeps its body
✅ got: #t
[🧪 test] The three pieces meet at line boundaries
[⚙️ assert] The library starts on a new line
✅ got: #t
[⚙️ assert] The postlude starts on a new line
✅ got: #t
[⚙️ assert] The rc ends with a newline
✅ got: #t
[🧪 test] An alias stays on one line
[⚙️ assert] A body with a newline would break the alias line
✅ got: 0
[🧪 test] The shared library is spliced, not referenced
[⚙️ assert] The library is read from the checkout
✅ got: # shellcheck shell=bash
# shellcheck disable=SC2034 # palette values are consumed indirectly (${!name})
# =============================================================================
# Shared shell library — wolk-jjba (this file) · heks-bash (the same file)
# Also mirrored for zsh in cask-and-cons/src/cask-and-cons/shell-lib.zsh.
#
# Keep this file identical to heks-bash/src/heks-bash/shell-lib.bash, header
# aside, so the two machines feel like one system. Everything
# machine-specific comes from the CC_* variables below, which the host config
# sets *before* calling `cc_boot':
#
# CC_FLAVOR name of the config, e.g. heks-bash
# CC_HOST_KIND at-a-glance machine kind, e.g. guix/linux
# CC_SIGIL prompt sigil, e.g. λ
# CC_ACCENT 256-colour accent (0-255), e.g. 99
# CC_RC_FILE file `reload' sources
# CC_PATH_DEPTH path components kept in the prompt (default 3)
#
# No plugins, no external prompt framework: the only fork per prompt is a
# single `git status --porcelain=v2 --branch'. The pager is plain less with
# the Emacs keymap from the sibling `lesskey' file, installed by the same
# installer that writes this rc.
# =============================================================================
CC_PATH_DEPTH=${CC_PATH_DEPTH:-3}
CC_ACCENT=${CC_ACCENT:-99}
: "${CC_FLAVOR:=shell}"
: "${CC_HOST_KIND:=unix}"
: "${CC_SIGIL:=λ}"
: "${CC_RC_FILE:=$HOME/.bashrc}"
# ---- history ----------------------------------------------------------------
# One big, shared, timestamped, de-duplicated history per machine.
HISTFILE=${HISTFILE:-$HOME/.bash_history}
HISTSIZE=${HISTSIZE:-500000}
HISTFILESIZE=${HISTFILESIZE:-100000}
HISTTIMEFORMAT=${HISTTIMEFORMAT:-'%F %T | '}
HISTCONTROL=${HISTCONTROL:-ignoreboth:erasedups}
HISTIGNORE=${HISTIGNORE:-'&:[ ]*:exit:ls:ll:l:bg:fg:history:clear:cd:cd ..:pwd'}
shopt -s histappend cmdhist checkwinsize 2> /dev/null
# ---- pager ------------------------------------------------------------------
# One pager everywhere (less), with Emacs key bindings: the keymap is the
# shared lesskey source file installed as $XDG_CONFIG_HOME/lesskey — less 582
# and later read it directly, so there is no compiled file to keep in sync.
# -F quit-if-one-screen, -I case-insensitive search, -M long prompt, -R colour.
export PAGER=${PAGER:-less}
export MANPAGER=${MANPAGER:-less}
export LESS="-F -I -M -R"
# less looks for that file on its own, so nothing needs to be exported; older
# less versions (< 582) simply ignore it and keep their default keys.
CC_LESSKEY=${XDG_CONFIG_HOME:-$HOME/.config}/lesskey
export CC_LESSKEY
# ---- palette ----------------------------------------------------------------
# 256-colour ANSI. `CC_PLAIN=1' (or NO_COLOR / TERM=dumb) renders the same
# prompt without escapes. \001 ... \002 is readline's \"this is not printable\".
__cc_palette () {
local v name
if [[ ${CC_PLAIN:-0} != 0 || -n ${NO_COLOR:-} || ${TERM:-dumb} == dumb ]]
then
for v in ACCENT TEXT DIM PATH BRANCH DIRTY AB BAD GOOD BOLD RESET
do
printf -v "CC_${v}_SEQ" '%s' ''
printf -v "CC_${v}_OUT" '%s' ''
done
return 0
fi
# plain escapes: used by normal output (greeting, about)
printf -v CC_RESET_OUT '\033[0m'
printf -v CC_BOLD_OUT '\033[1m'
printf -v CC_ACCENT_OUT '\033[38;5;%sm' "$CC_ACCENT"
printf -v CC_TEXT_OUT '\033[38;5;252m'
printf -v CC_DIM_OUT '\033[38;5;244m'
printf -v CC_PATH_OUT '\033[38;5;111m'
printf -v CC_BRANCH_OUT '\033[38;5;81m'
printf -v CC_DIRTY_OUT '\033[38;5;221m'
printf -v CC_AB_OUT '\033[38;5;176m'
printf -v CC_BAD_OUT '\033[38;5;203m'
printf -v CC_GOOD_OUT '\033[38;5;114m'
# prompt copies: \001 ... \002 are readline's "zero width" markers, so
# the prompt is measured correctly without counting the escapes.
for v in ACCENT TEXT DIM PATH BRANCH DIRTY AB BAD GOOD BOLD RESET
do
name=CC_${v}_OUT
printf -v "CC_${v}_SEQ" '\001%s\002' "${!name}"
done
}
# ---- prompt pieces ----------------------------------------------------------
# $HOME becomes ~, then only the last CC_PATH_DEPTH components are kept.
__cc_trim_path () {
local p=${PWD/#$HOME/\~} depth=$CC_PATH_DEPTH
local -a parts=()
local IFS=/
IFS=/ read -r -a parts <<< "$p"
local n=${#parts[@]}
if ((n <= depth))
then
printf '%s' "$p"
return 0
fi
local out='' i
for ((i = n - depth; i < n; i++))
do
out+=/${parts[i]}
done
printf '…%s' "$out"
}
# Environment badges: Guix shell, Nix shell, Python venv.
__cc_env_badge () {
if [[ -n ${GUIX_ENVIRONMENT:-} ]]
then
printf ' %s[guix env]%s' "$CC_DIM_SEQ" "$CC_RESET_SEQ"
elif [[ -n ${IN_NIX_SHELL:-} ]]
then
printf ' %s[nix]%s' "$CC_DIM_SEQ" "$CC_RESET_SEQ"
elif [[ -n ${VIRTUAL_ENV:-} ]]
then
printf ' %s[venv]%s' "$CC_DIM_SEQ" "$CC_RESET_SEQ"
fi
}
# One fork per prompt: porcelain v2 gives branch, upstream divergence and the
# dirty state in a single call. Untracked files are ignored (they are noise).
__cc_git_sync () {
CC_GIT_SEG=''
local dir=$PWD gd=''
while :
do
if [[ -e $dir/.git ]]
then
gd=$dir
break
fi
[[ $dir == / ]] && return 0
dir=${dir%/*}
[[ -z $dir ]] && dir=/
done
local out
out=$(GIT_OPTIONAL_LOCKS=0 git -C "$gd" status --porcelain=v2 --branch \
--ignore-submodules=dirty --untracked-files=no 2> /dev/null) || return 0
local branch='' oid='' ahead=0 behind=0 changed=0 conflicts=0 stash=0
local line ab
while IFS= read -r line
do
case $line in
'# branch.head '*)
branch=${line#'# branch.head '}
;;
'# branch.oid '*)
oid=${line#'# branch.oid '}
;;
'# branch.ab '*)
ab=${line#'# branch.ab '}
ahead=${ab%% *}
ahead=${ahead#+}
behind=${ab##* }
behind=${behind#-}
;;
'u '*)
conflicts=$((conflicts + 1))
;;
[12]' '*)
changed=$((changed + 1))
;;
esac
done <<< "$out"
[[ -z $branch ]] && return 0
[[ $branch == '(detached)' ]] && branch=@${oid:0:7}
# Stash entries are small plain-text logs; counting them costs no fork.
if [[ -f $gd/.git/logs/refs/stash ]]
then
while read -r _
do
stash=$((stash + 1))
done < "$gd/.git/logs/refs/stash"
fi
CC_GIT_SEG=" ${CC_BRANCH_SEQ}${branch}${CC_RESET_SEQ}"
if ((conflicts))
then
CC_GIT_SEG+=" ${CC_BAD_SEQ}✖${conflicts}${CC_RESET_SEQ}"
elif ((changed))
then
CC_GIT_SEG+=" ${CC_DIRTY_SEQ}✚${changed}${CC_RESET_SEQ}"
else
CC_GIT_SEG+=" ${CC_GOOD_SEQ}✓${CC_RESET_SEQ}"
fi
((ahead)) && CC_GIT_SEG+=" ${CC_AB_SEQ}↑${ahead}${CC_RESET_SEQ}"
((behind)) && CC_GIT_SEG+=" ${CC_AB_SEQ}↓${behind}${CC_RESET_SEQ}"
((stash)) && CC_GIT_SEG+=" ${CC_DIM_SEQ}⚑${stash}${CC_RESET_SEQ}"
return 0
}
# Two lines everywhere, identical on every machine:
# ╭─ user@host <sigil> <kind> ~/some/where [guix env]
# ╰─ main ✚2 ↑1 <sigil>
__cc_prompt () {
local cwd badge
cwd=$(__cc_trim_path)
badge=$(__cc_env_badge)
__cc_git_sync
PS1="${CC_DIM_SEQ}╭─ ${CC_RESET_SEQ}${CC_BOLD_SEQ}\u${CC_RESET_SEQ}"
PS1+="${CC_DIM_SEQ}@${CC_RESET_SEQ}\h "
PS1+="${CC_ACCENT_SEQ}${CC_SIGIL} ${CC_HOST_KIND}${CC_RESET_SEQ} "
PS1+="${CC_PATH_SEQ}${cwd}${CC_RESET_SEQ}${badge}"$'\n'
PS1+="${CC_DIM_SEQ}╰─${CC_RESET_SEQ}${CC_GIT_SEG} "
PS1+="${CC_ACCENT_SEQ}${CC_SIGIL}${CC_RESET_SEQ} "
history -a 2> /dev/null
return 0
}
# ---- greeting ---------------------------------------------------------------
# The pool of sayings (CC_GREETINGS) is data and lives in bash.scm
# (`bash-greetings'); it is emitted into the rc before this runs.
shell-greeting () {
local q=${CC_GREETINGS[RANDOM % ${#CC_GREETINGS[@]}]}
printf '%s%s%s %s%s%s %s%s%s\n\n' \
"$CC_ACCENT_OUT" "$CC_SIGIL" "$CC_RESET_OUT" \
"$CC_BOLD_OUT" "$CC_FLAVOR" "$CC_RESET_OUT" \
"$CC_DIM_OUT" "$q" "$CC_RESET_OUT"
}
# ---- self knowledge ---------------------------------------------------------
about () {
local shell_name="bash $BASH_VERSION"
printf '%s%s%s %s%s%s\n' \
"$CC_ACCENT_OUT" "$CC_SIGIL" "$CC_RESET_OUT" \
"$CC_BOLD_OUT" "$CC_FLAVOR" "$CC_RESET_OUT"
printf ' %skind%s %s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "$CC_HOST_KIND"
printf ' %shost%s %s@%s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "${USER:-$(id -un)}" "$(uname -n)"
printf ' %ssystem%s %s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "$(uname -srm)"
printf ' %sshell%s %s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "$shell_name"
printf ' %sconfig%s %s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "$CC_RC_FILE"
printf ' %sgit%s %s\n' "$CC_DIM_OUT" "$CC_RESET_OUT" "$(git --version 2> /dev/null | cut -d' ' -f3)"
printf ' %sprompt%s accent %s · sigil %s · depth %s · %s\n' \
"$CC_DIM_OUT" "$CC_RESET_OUT" "$CC_ACCENT" "$CC_SIGIL" "$CC_PATH_DEPTH" \
"$([[ ${CC_PLAIN:-0} != 0 || -n ${NO_COLOR:-} ]] && echo plain || echo colour)"
printf ' %spager%s less · Emacs keys · %s\n' \
"$CC_DIM_OUT" "$CC_RESET_OUT" "$CC_LESSKEY"
printf ' %shelpers%s about · pager-keys · hist-top · paths · extract · mkcd · up · serve · bak · calc · json · notify\n' \
"$CC_DIM_OUT" "$CC_RESET_OUT"
}
# -----------------------------------------------------------------------------
# Helper functions (vanilla: coreutils + git only)
# -----------------------------------------------------------------------------
__cc_has () { command -v "$1" > /dev/null 2>&1; }
mkcd () {
if [[ -z ${1:-} ]]
then
printf 'usage: mkcd <dir>\n' >&2
return 2
fi
mkdir -p -- "$1" && cd -- "$1" || return
}
up () {
local n=${1:-1} p=''
while ((n-- > 0))
do
p+=../
done
cd "$p" || return
pwd
}
extract () {
local f=${1:-}
if [[ ! -f $f ]]
then
printf 'extract: not a file: %s\n' "${f:-<none>}" >&2
return 1
fi
case $f in
*.tar.bz2 | *.tbz2) tar xjf "$f" ;;
*.tar.gz | *.tgz) tar xzf "$f" ;;
*.tar.xz | *.txz) tar xJf "$f" ;;
*.tar.zst) tar --zstd -xf "$f" ;;
*.tar) tar xf "$f" ;;
*.zip) unzip -q "$f" ;;
*.7z) 7z x "$f" ;;
*.rar) unrar x "$f" ;;
*.gz) gzip -dc "$f" > "${f%.gz}" ;;
*.bz2) bzip2 -dc "$f" > "${f%.bz2}" ;;
*.xz) xz -dc "$f" > "${f%.xz}" ;;
*.zst) zstd -dc "$f" > "${f%.zst}" ;;
*)
printf 'extract: unknown archive type: %s\n' "$f" >&2
return 1
;;
esac
}
psg () {
if [[ -z ${1:-} ]]
then
printf 'usage: psg <pattern>\n' >&2
return 2
fi
ps aux | grep -i -- "[${1:0:1}]${1:1}"
}
kport () {
local p=${1:-} pids=''
if [[ -z $p ]]
then
printf 'usage: kport <port>\n' >&2
return 2
fi
if __cc_has lsof
then
pids=$(lsof -ti "tcp:$p" 2> /dev/null)
elif __cc_has ss
then
pids=$(ss -tlnpH "sport = :$p" 2> /dev/null \
| grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u)
fi
if [[ -z $pids ]]
then
printf 'kport: nothing listening on %s\n' "$p" >&2
return 1
fi
kill $pids && printf 'kport: sent TERM to %s\n' "$pids"
}
serve () {
local p=${1:-8000}
printf 'serving %s on http://localhost:%s\n' "$PWD" "$p"
if __cc_has python3
then
python3 -m http.server "$p"
else
python -m SimpleHTTPServer "$p"
fi
}
# Desktop notification: notify-send where available, otherwise the terminal
# bell on the controlling TTY (so it is harmless in scripts and over SSH).
notify () {
local msg=${1:-done} title=${2:-$(hostname -s)}
if __cc_has notify-send
then
notify-send "$title" "$msg"
elif [[ -t 1 ]]
then
printf '\a%s: %s\n' "$title" "$msg"
else
printf '%s: %s\n' "$title" "$msg"
fi
}
# Listening sockets: ss on Linux, lsof on macOS/BSD.
ports () {
if __cc_has ss
then
ss -tulpn
else
lsof -iTCP -sTCP:LISTEN -P -n
fi
}
bak () {
local f=${1:-}
if [[ -z $f ]]
then
printf 'usage: bak <file>\n' >&2
return 2
fi
local d
d=$f.$(date +%Y%m%d-%H%M%S).bak
cp -p -- "$f" "$d" && printf '→ %s\n' "$d"
}
calc () {
if (($# == 0))
then
printf 'usage: calc <expression>\n' >&2
return 2
fi
LC_ALL=C awk "BEGIN { printf \"%.10g\\n\", $* }"
}
json () {
if __cc_has python3
then
python3 -m json.tool "$@"
else
cat "$@"
fi
}
paths () {
local IFS=: i=0 p
for p in $PATH
do
printf '%2d %s\n' "$i" "$p"
i=$((i + 1))
done
}
# The pager keymap (installed as $XDG_CONFIG_HOME/lesskey) in one screen.
pager-keys () {
local b=${CC_BOLD_OUT:-} d=${CC_DIM_OUT:-} r=${CC_RESET_OUT:-}
local a=${CC_ACCENT_OUT:-}
printf '%s%spager%s less · Emacs keys · %s\n\n' \
"$b" "$a" "$r$d" "$CC_LESSKEY"
printf ' %s%-11s%s %s\n' "$b" 'C-n C-p' "$r" 'one line down / up'
printf ' %s%-11s%s %s\n' "$b" 'C-f C-b' "$r" 'one screen down / up'
printf ' %s%-11s%s %s\n' "$b" 'C-v M-v' "$r" 'one screen down / up'
printf ' %s%-11s%s %s\n' "$b" 'C-d C-u' "$r" 'half a screen down / up'
printf ' %s%-11s%s %s\n' "$b" 'C-a C-e' "$r" 'top / bottom of the file'
printf ' %s%-11s%s %s\n' "$b" 'M-< M->' "$r" 'top / bottom of the file'
printf ' %s%-11s%s %s\n' "$b" 'C-s C-r' "$r" 'search forward / backward'
printf ' %s%-11s%s %s\n' "$b" 'M-g' "$r" 'go to line'
printf ' %s%-11s%s %s\n' "$b" 'M-u' "$r" 'clear the search highlighting'
printf ' %s%-11s%s %s\n' "$b" 'C-l' "$r" 'redraw the screen'
printf '\n %sin prompts (:e, /, ?, ...)%s\n' "$d" "$r"
printf ' %s%-11s%s %s\n' "$b" 'C-a C-e' "$r" 'beginning / end of the line'
printf ' %s%-11s%s %s\n' "$b" 'C-b C-f' "$r" 'one character left / right'
printf ' %s%-11s%s %s\n' "$b" 'M-b M-f' "$r" 'one word left / right'
printf ' %s%-11s%s %s\n' "$b" 'C-w M-d' "$r" 'kill the word before / after the cursor'
printf ' %s%-11s%s %s\n' "$b" 'C-k C-u' "$r" 'kill the whole line'
printf ' %s%-11s%s %s\n' "$b" 'C-g' "$r" 'cancel the prompt'
printf '\n %sstill plain less%s: space b · g G · / ? · n N · m '\'' · ! · | · q%s\n' \
"$d" "$r" "$r"
}
reload () {
printf 'reloading %s\n' "$CC_RC_FILE"
# shellcheck source=/dev/null
CC_RELOAD=1 . "$CC_RC_FILE"
}
cdroot () {
local d=$PWD
while :
do
if [[ -e $d/.git ]]
then
cd "$d" || return 1
return 0
fi
[[ $d == / ]] && break
d=${d%/*}
[[ -z $d ]] && d=/
done
printf 'cdroot: not inside a git work tree\n' >&2
return 1
}
ff () {
if [[ -z ${1:-} ]]
then
printf 'usage: ff <name-fragment>\n' >&2
return 2
fi
find . -iname "*${1}*" -not -path './.git/*' -not -path './node_modules/*'
}
# Most frequently used commands, straight from the history file.
hist-top () {
local n=${1:-10} f=${HISTFILE:-$HOME/.bash_history}
[[ -r $f ]] || return 0
awk '!/^#/ && NF { print $1 }' "$f" | sort | uniq -c | sort -rn | head -n "$n"
}
hgrep () {
history | grep -- "${1:-}"
}
# Decompress a .gz log and page through it.
gunlog () {
if [[ -z ${1:-} ]]
then
printf 'usage: gunlog <file.gz>\n' >&2
return 2
fi
gzip -dc -- "$1" | ${PAGER:-less}
}
# Startup cost of the shell itself, averaged over a few runs.
rc-time () {
local n=${1:-5} t0 t1 i
t0=${EPOCHREALTIME//,/.} # EPOCHREALTIME honours LC_NUMERIC
for ((i = 0; i < n; i++))
do
bash -i -c exit > /dev/null 2>&1
done
t1=${EPOCHREALTIME//,/.}
LC_ALL=C awk -v a="$t0" -v b="$t1" -v n="$n" \
'BEGIN { printf "%.1f ms per start (%d runs)\n", (b - a) * 1000 / n, n }'
}
# ---- git -------------------------------------------------------------------
# The one-line git shortcuts (gst, gd, gco, ...) are generated from
# `bash-git-shortcuts' in bash.scm; only the multi-line ones stay here.
glog () {
local n=${1:-20}
git log --graph --abbrev-commit --decorate --date=short \
--format='%C(auto)%h%C(reset) %C(auto)%d%C(reset) %s %C(dim)(%ar)%C(reset)' \
-n "$n" --all
}
# -----------------------------------------------------------------------------
# Boot: colours, prompt hook, greeting. Called once at the end of the rc file.
# -----------------------------------------------------------------------------
cc_boot () {
# PS1 is already fully expanded here; leaving promptvars on would let a
# directory called "$(cmd)" run cmd every time the prompt is drawn.
shopt -u promptvars 2> /dev/null
# No XOFF/XON: C-s is forward-search in readline and in the pager, not a
# frozen terminal. Set per TTY, and only when there is one.
[[ -t 0 ]] && stty -ixon 2> /dev/null
__cc_palette
case ${PROMPT_COMMAND:-} in
*__cc_prompt*) ;;
'') PROMPT_COMMAND=__cc_prompt ;;
*) PROMPT_COMMAND="__cc_prompt;$PROMPT_COMMAND" ;;
esac
__cc_prompt
[[ ${CC_RELOAD:-0} == 1 ]] || shell-greeting
return 0
}
[⚙️ assert] No store path leaks into the rc
✅ got: #f
[⚙️ assert] The spliced text is the library itself
✅ got: #t
[🧪 test] Git shortcuts are generated as functions
[⚙️ assert] gst keeps its body and its arguments
✅ got: #t
[⚙️ assert] groot calls the shared cdroot
✅ got: #t
[⚙️ assert] The one-line shortcuts are no longer in the library
✅ got: #f
[⚙️ assert] The multi-line glog stays in the library
✅ got: #t
[🧪 test] History stays shell-local
[⚙️ assert] CDPATH is exported for child shells
✅ got: #t
[⚙️ assert] The history size is set
✅ got: #t
[⚙️ assert] The history size is not pushed into every subprocess
✅ got: #f
[🧪 test] No alias shadows a function
[⚙️ assert] Shared aliases are not git shortcuts
✅ got: ()
[⚙️ assert] Shared aliases are not server helpers
✅ got: ()
[⚙️ assert] Machine aliases are not server helpers
✅ got: ()
[🧪 test] The prompt says which machine this is
[⚙️ assert] The flavour is the repository name
✅ got: #t
[⚙️ assert] The badge says this is a Guix server
✅ got: #t
[⚙️ assert] The sigil is the server's omega
✅ got: #t
[⚙️ assert] The accent is the server's teal
✅ got: #t
[⚙️ assert] Identity is an assignment, not exported to children
✅ got: #f
[🧪 test] The shared library still defines what the data calls
[⚙️ assert] cc_boot is defined where the rc calls it
✅ got: #t
[⚙️ assert] The hg alias has its hgrep
✅ got: #t
[⚙️ assert] groot has its cdroot
✅ got: #t
[⚙️ assert] The greeting function reads the generated array
✅ got: #t
[⚙️ assert] The file ends on a line of its own
✅ got: #t
[🧪 test] The rc boots exactly once, at the end
[⚙️ assert] cc_boot is called once in the postlude
✅ got: 1
[⚙️ assert] Nothing runs after the prompt is installed
✅ got: #t
[🧪 test] Every machine-identity variable is emitted
[⚙️ assert] The five CC_* values are declared
✅ got: 5
[⚙️ assert] Each one reaches the rc verbatim
✅ got: ()
[🧪 test] Greetings survive quotes, dollars and backticks
[⚙️ assert] Every shell-special character is escaped
✅ got: CC_GREETINGS=(
"a \"quoted\" \$HOME \`date\` \\ end"
)
[🧪 test] The greeting array is emitted once, before it is read
[⚙️ assert] The array is declared in the data block
✅ got: 1
[⚙️ assert] The shared library no longer carries the pool
✅ got: #f
[🧪 test] The greeting pool is non-empty and unique
[⚙️ assert] shell-greeting must always find a saying
✅ got: #t
[⚙️ assert] No saying is declared twice
✅ got: 48
ℹ️ info - ran 1 tasks in: 0 seconds, 22646 microseconds
running unit tests from file: containers-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Containers Suite
[🧪 test] A Docker client is handed the socket the machine serves
[⚙️ assert] The socket lives beside the user's runtime dir
✅ got: /run/user/1002/podman/podman.sock
[⚙️ assert] The socket has a directory of its own
✅ got: /run/user/1002/podman
[⚙️ assert] DOCKER_HOST is a unix:// URL, not a path
✅ got: unix:///run/user/1002/podman/podman.sock
[⚙️ assert] The runtime directory is XDG's, by uid
✅ got: /run/user/0
[🧪 test] Both accounts that build in containers are configured
[⚙️ assert] joe builds in containers by hand
✅ got: #t
[⚙️ assert] the CI builds in containers as its own user
✅ got: #t
[⚙️ assert] Every podman user has a subuid range
✅ got: (joe byggsteg-runner)
[⚙️ assert] Every podman user has a subgid range
✅ got: (joe byggsteg-runner)
[⚙️ assert] Each range is the kernel's usual 65536 IDs
✅ got: (65536 65536)
[⚙️ assert] The cgroup group owns the cgroup tree
✅ got: cgroup
[🧪 test] The API logs are rotated with the rest
[⚙️ assert] One log per API socket, plus the secrets timer
✅ got: (/var/log/wolk-jjba/secrets-apply.log /var/log/wolk-jjba/podman-api-joe.log /var/log/wolk-jjba/podman-api-byggsteg-runner.log)
[⚙️ assert] Every log the capability writes is rotated
✅ got: (/var/log/wolk-jjba/secrets-apply.log /var/log/wolk-jjba/podman-api-joe.log /var/log/wolk-jjba/podman-api-byggsteg-runner.log)
[🧪 test] One session service prepares every account's runtime directory
[⚙️ assert] There is a sessions service
✅ got: #t
[⚙️ assert] It is provisioned once, under one name
✅ got: (podman-user-sessions)
[⚙️ assert] It is a one-shot: nothing to keep running
✅ got: #t
[⚙️ assert] It creates the XDG runtime directory
✅ got: #t
[⚙️ assert] It does it for every podman user
✅ got: #t
[⚙️ assert] It asks elogind to keep the directory
✅ got: #t
[🧪 test] The registry order is this machine's
[⚙️ assert] docker.io is searched first
✅ got: #t
[⚙️ assert] nothing here comes from the Fedora registry
✅ got: #f
[🧪 test] The profile has everything a rootless build needs
[⚙️ assert] The storage driver's mount program is installed
✅ got: #t
[⚙️ assert] The fallback user-mode network is installed
✅ got: #t
[⚙️ assert] A `docker' command that is Podman is installed
✅ got: #t
[🧪 test] Every podman user gets a Docker-compatible API socket
[⚙️ assert] One API service per account
✅ got: 2
[⚙️ assert] joe can run a testcontainers suite
✅ got: #t
[⚙️ assert] the CI can start a container through the API
✅ got: #t
[⚙️ assert] The service is `podman system service'
✅ got: #t
[⚙️ assert] An API socket is not allowed to time out
✅ got: #t
[⚙️ assert] It speaks HTTP over a unix socket
✅ got: #t
[⚙️ assert] The socket is the one DOCKER_HOST names
✅ got: #t
[⚙️ assert] The cgroup group survives the privilege drop
✅ got: #t
[⚙️ assert] The setuid helpers come first in PATH
✅ got: #t
[⚙️ assert] A killed socket is brought back
✅ got: #t
[🧪 test] The storage driver works without root
[⚙️ assert] overlay is given a mount program
✅ got: #t
[⚙️ assert] The driver is still overlay
✅ got: #t
[🧪 test] The activation prepares sessions and shares the checkout
[⚙️ assert] Runtime directories exist before any service
✅ got: #t
[⚙️ assert] The Byggsteg tree is made group-writable
✅ got: #t
[⚙️ assert] Only directories gain an execute bit
✅ got: #t
[🧪 test] The engine does not assume systemd
[⚙️ assert] Guix System has no systemd to ask for a cgroup
✅ got: #t
[⚙️ assert] Rootless networking uses the packaged pasta
✅ got: #t
[🧪 test] containers.conf is installed beside the files Guix writes
[⚙️ assert] The fourth /etc/containers file is added
✅ got: (containers/containers.conf)
[🧪 test] A podman process is given a home and a runtime directory
[⚙️ assert] The storage graph root follows HOME
✅ got: #t
[⚙️ assert] Podman refuses a runtime dir that is not there
✅ got: #t
[⚙️ assert] A Docker client started by a build finds the API
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 12337 microseconds
running unit tests from file: temp-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Temporary Directory Suite
[🧪 test] A directory that is already there is never handed out
[⚙️ assert] The second call chose another name
✅ got: #t
[⚙️ assert] The new fixture does not inherit the last run's files
✅ got: #t
[⚙️ assert] And the directory that was there is left alone
✅ got: #t
[🧪 test] Two calls never make the same directory
[⚙️ assert] Every fixture has a name of its own
✅ got: #t
[⚙️ assert] And every one of them was created
✅ got: #t
[⚙️ assert] Each starts empty, so no run inherits files
✅ got: #t
[⚙️ assert] And none of them is readable by another user
✅ got: #t
[🧪 test] The directory is named after the fixture, in TMPDIR
[⚙️ assert] The prefix names the suite that made it
✅ got: #t
[⚙️ assert] And it is created in the directory asked for
✅ got: #t
[🧪 test] Removal takes the whole tree, and a missing path is fine
[⚙️ assert] Removal reports success
✅ got: #t
[⚙️ assert] The tree is gone, not just its files
✅ got: #t
[⚙️ assert] Removing what is not there is not an error
✅ got: #t
[⚙️ assert] Neither is a path that never existed
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 6520 microseconds
running unit tests from file: sysctl-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Sysctl Suite
[🧪 test] Every setting is a well-formed key/value pair
[⚙️ assert] Each entry is a dotted key and a string
✅ got: #t
[⚙️ assert] No key is set twice
✅ got: 30
[🧪 test] The settings extend Guix's defaults instead of replacing them
[⚙️ assert] Guix's own defaults survive
✅ got: #t
[⚙️ assert] and this module's settings are added
✅ got: #t
[⚙️ assert] Nothing is set twice or dropped
✅ got: 32
[🧪 test] Spoofed and redirected traffic is not honoured
[⚙️ assert] Reverse-path filtering is strict
✅ got: 1
[⚙️ assert] and applies to interfaces created later
✅ got: 1
[⚙️ assert] IPv4 redirects are refused
✅ got: 0
[⚙️ assert] IPv6 redirects are refused
✅ got: 0
[⚙️ assert] This host sends no redirects
✅ got: 0
[⚙️ assert] Source-routed IPv4 is refused
✅ got: 0
[🧪 test] The kernel survives a SYN flood
[⚙️ assert] Syncookies are on
✅ got: 1
[⚙️ assert] The SYN backlog is larger than the default
✅ got: #t
[⚙️ assert] The accept queue is larger than the default
✅ got: #t
[🧪 test] ICMP is bounded
[⚙️ assert] Broadcast echo is ignored
✅ got: 1
[⚙️ assert] Echo replies are rate limited
✅ got: #t
[🧪 test] Kernel internals are not readable by ordinary users
[⚙️ assert] Kernel pointers are hidden
✅ got: 2
[⚙️ assert] Only root reads the ring buffer
✅ got: 1
[⚙️ assert] Unprivileged BPF loading is off
✅ got: 1
[🧪 test] The host keeps its routing and addressing role
[⚙️ assert] Packet forwarding is left to Podman
✅ got: #f
[⚙️ assert] IPv6 forwarding is left alone
✅ got: #f
[⚙️ assert] Router advertisements stay with NetworkManager
✅ got: #f
[⚙️ assert] IPv6 is not switched off
✅ got: #f
ℹ️ info - ran 1 tasks in: 0 seconds, 9244 microseconds
running unit tests from file: certbot-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Certbot Suite
[🧪 test] Every certificate reloads nginx
[⚙️ assert] A deploy hook is attached to each request
✅ got: #t
[⚙️ assert] All of them are the one nginx reload hook
✅ got: #t
[⚙️ assert] The hook reads nginx's pid file
✅ got: #t
[⚙️ assert] The hook reloads nginx with SIGHUP
✅ got: #t
[🧪 test] The capability keeps the documented account and webroot
[⚙️ assert] The ACME account address is set
✅ got: jjbigorra@gmail.com
[⚙️ assert] HTTP-01 challenges are answered from /srv/http
✅ got: /srv/http
[🧪 test] Each vhost has a certificate
[⚙️ assert] Every TLS vhost name is covered by a certificate
✅ got: ()
[🧪 test] Fifteen certificates are requested
[⚙️ assert] The count matches the list in the module
✅ got: 15
[⚙️ assert] No domain is requested twice
✅ got: 28
[🧪 test] Each certificate has a vhost
[⚙️ assert] No certificate is requested for an unserved name
✅ got: ()
[🧪 test] A certificate request covers the apex and its www
[⚙️ assert] The www alias is requested by default
✅ got: (example.org www.example.org)
[⚙️ assert] with-www? #f requests the apex alone
✅ got: (naked.example)
ℹ️ info - ran 1 tasks in: 0 seconds, 4871 microseconds
running unit tests from file: prelude-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Prelude Suite
[🧪 test] Every factory call yields an independent service type
[⚙️ assert] Two daemons are two service types
✅ got: #f
[🧪 test] The daemon environment carries a UTF-8 locale
[⚙️ assert] LANG is set for every application daemon
✅ got: #t
[⚙️ assert] Nothing forces the C locale
✅ got: #f
[⚙️ assert] Guile finds the system's modules
✅ got: #t
[⚙️ assert] The environment is the declared eleven
✅ got: 11
[⚙️ assert] A PATH is passed, so make and guile resolve
✅ got: #t
[🧪 test] Shell helpers return what the command printed
✅ got: hello
[🧪 test] Managed projects live under one checkout tree
[⚙️ assert] The default branch is trunk
✅ got: /etc/byggsteg/job-clone/lucidplan/trunk
[⚙️ assert] A non-default branch is honoured
✅ got: /etc/byggsteg/job-clone/wikimusic/experiment
[⚙️ assert] The name may be any symbol
✅ got: /etc/byggsteg/job-clone/byggsteg/trunk
[🧪 test] The daemon runs maak in the project directory
[⚙️ assert] The default command is make production-server
✅ got: #t
[⚙️ assert] The command is maak, run through make
✅ got: #t
[⚙️ assert] The project directory is the working one
✅ got: #t
[⚙️ assert] A working directory reaches the constructor
✅ got: #t
[🧪 test] A daemon is a respawning, auto-started shepherd service
[⚙️ assert] The provision name is the daemon name
✅ got: (probe-daemon)
[⚙️ assert] A crashed daemon must be respawned by the Shepherd
✅ got: #t
[⚙️ assert] A daemon starts with the system
✅ got: #t
[⚙️ assert] The factory imposes no ordering of its own
✅ got: ()
[🧪 test] The factory names its own service type
[⚙️ assert] The service type is named after the daemon
✅ got: named-daemon
[⚙️ assert] The description is the documentation
✅ got: documented
[⚙️ assert] The service carries no value of its own
✅ got: ()
[🧪 test] The log file can be overridden
[⚙️ assert] The explicit path wins over the convention
✅ got: #t
[⚙️ assert] The conventional path is not also passed
✅ got: #f
[🧪 test] The daemon logs to its own file, not the system log
[⚙️ assert] The log file is named after the daemon
✅ got: #t
[⚙️ assert] The log directory is created before exec
✅ got: #t
[⚙️ assert] The file is handed to #:log-file
✅ got: #t
[🧪 test] Per-daemon logs share one directory
[⚙️ assert] The log directory is the one Alloy tails
✅ got: /var/log/wolk-jjba
ℹ️ info - ran 1 tasks in: 0 seconds, 13017 microseconds
running unit tests from file: logging-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 194873666 (replay with VERITAS_SEED=194873666)
+----------------------------------------------+
[📑 suite] Logging Suite
[🧪 test] The capability is a rewriting function, not a service
[⚙️ assert] The service list it is given comes back
✅ got: 1
[🧪 test] The capability replaces the Shepherd's destination
[⚙️ assert] The system log service survives the rewrite
✅ got: #tgfs-backup: proj failed: misc-error (#f "~A" ("cannot reach the Storage Box over ssh") #f)
gfs-backup: other failed: misc-error (#f "~A" ("cannot reach the Storage Box over ssh") #f)
[⚙️ assert] The configured destination is this module's procedure
✅ got: #t
[⚙️ assert] It is not the default catch-all destination
✅ got: #f
[🧪 test] Everything else lands in the catch-all
[⚙️ assert] There is a catch-all file
✅ got: #t
[⚙️ assert] Private authorization messages are routed too
✅ got: #t
[⚙️ assert] Facilities are read through the Shepherd
✅ got: #t
[🧪 test] Urgent messages reach the console
[⚙️ assert] emergency and alert are written to /dev/console
✅ got: #t
[⚙️ assert] The emergency priority is named in the table
✅ got: #t
[⚙️ assert] The alert priority is named in the table
✅ got: #t
[⚙️ assert] Priorities are read through the Shepherd
✅ got: #t
[🧪 test] AUTH, mail and cron have their own files
[⚙️ assert] Authentication goes to auth.log
✅ got: #t
[⚙️ assert] Mail goes to mail.log
✅ got: #t
[⚙️ assert] Scheduled jobs go to cron.log
✅ got: #t
[⚙️ assert] Kernel messages go to kern.log
✅ got: #t
[⚙️ assert] Daemon messages go to daemon.log
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 11778 microseconds
finished running tests
;;; note: source file /etc/byggsteg/job-clone/wolk-jjba/trunk/maak.scm
;;; newer than compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/wolk-jjba/trunk/maak.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling /etc/byggsteg/job-clone/wolk-jjba/trunk/maak.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.6/etc/byggsteg/job-clone/wolk-jjba/trunk/maak.scm.go
--------------------------------------------------------------
Maak 0.14.0: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/wolk-jjba/trunk/maak.scm
Executing Maak tasks: test
--------------------------------------------------------------
=> Running Maak task: test
Run the Veritas unit-test suite, one process per test file.
Runs inside the manifest shell like the other Scheme tasks: the suite
imports (guix gexp) and (gnu services ...) and needs veritas, none of
which are in the machine's system profile, so a bare `guile' fails with
`no code for module (veritas runner)' -- which is what the Byggsteg CI
task `maak-test' used to hit.
The files run in parallel, one process each, in as many processes as
this machine may use up to four; VERITAS_JOBS overrides that count.
Four things are deliberate, so that a failure here is a failure of the
code under test rather than of the schedule:
- the order the files run in is shuffled, and the seed is printed:
`VERITAS_SEED=<the seed> VERITAS_SHUFFLE_FILES=true maak test'
replays a failing order, and `VERITAS_SHUFFLE_FILES=false' runs them
in the fixed one;
- every fixture is created under a name unique to the run and removed
when it ends (see test/veritas/temp.scm), so two workers -- or two CI
jobs sharing /tmp -- cannot collide on one;
- GUILE_LOAD_PATH names the channel's module directory, because
evaluating config.scm needs the jointhefreeworld channel and the pinned
shell does not carry it -- worked out from the pulled guix when there is
one and from the guix that pinned the channels when there is not (see
the comments above);
- a suite that needs the network or the machine's own root-only files
is a suite that fails for reasons nobody can act on, and there is
none of those: see the Commentary of each spec.
Executing: env VERITAS_JOBS=1 GUILE_LOAD_PATH=/gnu/store/8vr2vg156l14lklzyr49ymnd2fv5njfr-jointhefreeworld/share/guile/site/3.0 guix time-machine --channels=channels.scm -- shell -m manifest.scm -- env GUILE_AUTO_COMPILE=0 XDG_CACHE_HOME=/tmp/wolk-jjba-test-27644-cache guile -L ./src -L ./test -c '(use-modules (veritas runner)) (run-tests)'