baibot is the first bot rather than a bridge, and the shape
differs from the appservices: it is a plain Matrix client that logs in with a
password, sets up its profile and then syncs. It publishes no port, so nothing
can be probed over HTTP; what it says about itself in the journal is the only
window into whether the role's configuration reached the process.
What the scenario proves:
- The unit is active with no automatic restarts, AND baibot got past startup
into its sync loop. The second half is what carries the scenario. baibot never
exits when startup goes wrong - it retries the failing step forever with a
growing delay - so the unit sits there `active` with `NRestarts` at 0 while
the bot is permanently half-started. Pointing `user.avatar` at a file that is
not there reproduces exactly that: the unit assertion still passes, the sync
assertion does not.
- The display name the bot announces it wants is the role's `user.name`, which
is neither the role's default nor what the stub reports the account already
has.
- The rendered `logging` string took effect per target: baibot's own records
appear at DEBUG (the role ships `info`) while everything underneath stays at
the `warn` catch-all. The second half is the control, and raising the
catch-all turns 2 DEBUG records into 161.
- The rendered config carries the scenario's homeserver, identity, command
prefix, admin patterns and user patterns, and uses password authentication
exclusively, with the access-token keys rendered as nulls.
- The statically-defined agent survived the provider templating - the
per-provider template rendered to YAML, parsed, merged and nested into the
list - key by key.
- The container runs as the uid/gid the playbook supplies (1234, not the 1000
the base image already has), on the image version defaults/main.yml pins, and
could write its session into the data path.
No AI provider is contacted and none is needed. baibot calls a provider only
when a message asks an agent to do something, so a static agent with a
placeholder key and a base URL that resolves nowhere still has to survive the
bot's startup parsing - which is the part worth testing.
The shared stub grew what a syncing Matrix client needs and an appservice did
not: /sync (with a `next_batch`, and holding the call open for the timeout the
client asked for, or the bot spins the stub in a hot loop), the media config
and upload endpoints a bot setting its own avatar insists on, /keys/upload with
its key counts, and filter creation. Without the media config in particular,
baibot never gets past profile setup.
The shared stub task gained a STUB_VERBOSE knob. The stub already advertised
the environment variable but there was no way to set it from a scenario, and
for a component with no port of its own its request log is the only place to
see what the component is actually asking for.
Note: molecule-shared/homeserver-stub.py also carries a loosened /login match
from another scenario being written in this same tree at the same time; it was
already in the working copy and is not mine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEH3vxYSQ5SV4N5z61eyGT
Molecule checks for a requirements file at the scenario's default path
before it installs anything, and warns "Missing roles requirements file"
when it is absent - so `requirements-file` pointing elsewhere was silently
ignored and the galaxy roles were never installed. CI failed on a missing
ansible-role-docker; the local run passed only because those roles were
already in ~/.ansible/roles from earlier work, which is exactly the kind of
difference a clean CI run exists to expose.
A symlink satisfies Molecule's check while keeping one copy of the pins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things that would not have scaled to 70 roles:
- The Python and Ansible dependency pins were about to be copied into every
role. They now live once in molecule-shared/, which scenarios reference
relatively, so they cannot drift apart.
- The helper container images used for probing were hardcoded inline. They
are pinned once in molecule-shared/vars.yml, carry `# renovate:`
annotations, and a custom manager in .github/renovate.json keeps them
current - verified with a local Renovate dry run, which offers
curl 8.11.1 -> 8.21.0 and python 3.13 -> 3.14-alpine. Seventy invisible
hardcodes is the blindness class we have been removing elsewhere.
- Running a scenario meant knowing the venv and cd incantation. `just
molecule <role>` does it, and with no argument lists the roles that have
a scenario.
Molecule is deliberately not wired into prek: a run takes minutes, pulls
images and needs Docker, which is fine on request and not fine per commit.
docs/molecule-testing.md covers how to run and write these, including the
four things a role here needs that a standalone role does not. AGENTS.md
points at it rather than carrying the detail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Not for merging as-is. One role's scenario plus the workflow that would
make per-role testing affordable here, so we can see how it behaves before
deciding whether to do the other 69.
Unlike the MASH repositories, one repository holds every role, so running
everything on every push is not an option. The workflow's first job works
out which roles a push touched and builds the matrix from that; a change to
docs, or to a role with no scenario yet, runs nothing.
Three things this role needed that a MASH role does not:
- The variables matrix-base would supply (matrix_base_data_path,
matrix_domain, matrix_user_name, matrix_group_name and the uid/gid) have
to be provided by the scenario, and the user and group have to exist
before the role's file tasks run.
- The service contacts the homeserver while starting up - it fetches
/_matrix/client/v3/joined_rooms to resolve its room mapping and exits 1
if that fails - so prepare.yml stands up a stub homeserver. Most roles
here are bridges and bots, so this is likely the rule rather than the
exception.
- verify.yml runs as its own play, where role defaults are out of scope, so
the paths it reads are pinned in molecule.yml. The version is deliberately
NOT pinned: it is read from defaults/main.yml so the assertion compares
the running image against what the role ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>