wolk-jjba-ff97d8f5-6e2b

job succeeded

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)'
inspect the job's lambda expression
(lambda()
  `((task . "maak-test")
    (project . "wolk-jjba")
    (branch-name . "trunk")
    (clone-url . "https://codeberg.org/jjba23/wolk-jjba.git")))