Преглед изворни кода

Add a Molecule scenario for baibot, and teach the stub to answer a syncing client

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
pull/5575/head
Slavi Pantaleev пре 14 часа
родитељ
комит
6bdcddb79a
7 измењених фајлова са 526 додато и 2 уклоњено
  1. +50
    -2
      molecule-shared/homeserver-stub.py
  2. +6
    -0
      molecule-shared/tasks/homeserver-stub.yml
  3. +43
    -0
      roles/custom/matrix-bot-baibot/molecule/default/converge.yml
  4. +93
    -0
      roles/custom/matrix-bot-baibot/molecule/default/molecule.yml
  5. +85
    -0
      roles/custom/matrix-bot-baibot/molecule/default/prepare.yml
  6. +1
    -0
      roles/custom/matrix-bot-baibot/molecule/default/requirements.yml
  7. +248
    -0
      roles/custom/matrix-bot-baibot/molecule/default/verify.yml

+ 50
- 2
molecule-shared/homeserver-stub.py Прегледај датотеку

@@ -25,7 +25,9 @@ import json
import os import os
import re import re
import sys import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse


SERVER_NAME = os.environ.get("STUB_SERVER_NAME", "molecule.local") SERVER_NAME = os.environ.get("STUB_SERVER_NAME", "molecule.local")
PORT = int(os.environ.get("STUB_PORT", "8008")) PORT = int(os.environ.get("STUB_PORT", "8008"))
@@ -37,6 +39,13 @@ JOINED_ROOMS = [r for r in os.environ.get("STUB_JOINED_ROOMS", "").split(",") if


USER_ID = os.environ.get("STUB_USER_ID", f"@stub:{SERVER_NAME}") USER_ID = os.environ.get("STUB_USER_ID", f"@stub:{SERVER_NAME}")


# Longest a /sync call is held open. Long-polling clients (anything on
# matrix-sdk: baibot and the other bots) ask for a 30s timeout and immediately
# ask again when the call returns, so answering instantly would spin them into a
# hot loop that eats the test machine. Honouring the requested timeout, capped
# here, keeps an idle bot idle.
SYNC_MAX_HOLD_SECONDS = 30



class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" protocol_version = "HTTP/1.1"
@@ -50,7 +59,40 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(body) self.wfile.write(body)


def _route(self): def _route(self):
path = self.path.split("?", 1)[0]
parsed = urlparse(self.path)
path = parsed.path

# A client that syncs (every bot here does) needs a `next_batch` back or
# the response will not deserialize, and it needs the call to block for
# the timeout it asked for or it will hammer this stub. Nothing is ever
# reported: an idle bot is what a scenario wants.
if path.endswith("/sync"):
requested_ms = parse_qs(parsed.query).get("timeout", ["0"])[0]
try:
hold = min(int(requested_ms) / 1000.0, SYNC_MAX_HOLD_SECONDS)
except ValueError:
hold = 0
if hold > 0:
time.sleep(hold)
return {"next_batch": "molecule-stub-batch"}

# Before the generic `/upload` below: this one is the end-to-end
# encryption key upload, and the client insists on the key counts.
if path.endswith("/keys/upload"):
return {"one_time_key_counts": {}}

# Media. A bot that sets its own avatar asks for the upload limits first
# and refuses to proceed without them, then uploads and expects an MXC
# URI back.
if path.endswith("/media/config") or path.endswith("/media/v3/config"):
return {"m.upload.size": 10485760}

if path.endswith("/upload"):
return {"content_uri": f"mxc://{SERVER_NAME}/molecule-stub-media"}

# Sync filters are uploaded before the first sync and referenced by id.
if path.endswith("/filter"):
return {"filter_id": "molecule-stub-filter"}


if path.endswith("/joined_rooms"): if path.endswith("/joined_rooms"):
return {"joined_rooms": JOINED_ROOMS} return {"joined_rooms": JOINED_ROOMS}
@@ -67,7 +109,13 @@ class Handler(BaseHTTPRequestHandler):
if path.endswith("/capabilities"): if path.endswith("/capabilities"):
return {"capabilities": {}} return {"capabilities": {}}


if path.endswith("/_matrix/client/v3/login"):
# Bots that authenticate with a username and password rather than as an
# appservice with a token log in here. Matched loosely on purpose:
# clients differ on the API version prefix (matrix-nio has shipped both
# /_matrix/client/r0/login and /_matrix/client/v3/login over time), and a
# login that falls through to the catch-all `{}` below looks to the
# client like bad credentials.
if path.endswith("/login"):
return { return {
"user_id": USER_ID, "user_id": USER_ID,
"access_token": "stub_access_token", "access_token": "stub_access_token",


+ 6
- 0
molecule-shared/tasks/homeserver-stub.yml Прегледај датотеку

@@ -54,6 +54,12 @@
# returned is not the bot user they were configured as, so a scenario # returned is not the bot user they were configured as, so a scenario
# bridging anything has to tell the stub who it should claim to be. # bridging anything has to tell the stub who it should claim to be.
- --env=STUB_USER_ID={{ molecule_shared_stub_user_id | default('@stub:' + (molecule_shared_stub_server_name | default('molecule.local'))) }} - --env=STUB_USER_ID={{ molecule_shared_stub_user_id | default('@stub:' + (molecule_shared_stub_server_name | default('molecule.local'))) }}
# Off by default. Set molecule_shared_stub_verbose to "1" to have the stub
# log every request it is asked for, which is both how you find out why a
# component will not start and - for a component that exposes no port of
# its own - the only place a scenario can observe it acting on what the
# role configured.
- --env=STUB_VERBOSE={{ molecule_shared_stub_verbose | default('') }}
- --volume=/root/molecule-homeserver-stub.py:/stub.py:ro - --volume=/root/molecule-homeserver-stub.py:/stub.py:ro
- "{{ molecule_shared_image_python }}" - "{{ molecule_shared_image_python }}"
- python3 - python3


+ 43
- 0
roles/custom/matrix-bot-baibot/molecule/default/converge.yml Прегледај датотеку

@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
# The devture base roles carry the variables this role reads
# (`devture_systemd_docker_base_*`, `devture_playbook_help_*`), the same way
# they do when the playbook runs. `matrix-base` is deliberately NOT included:
# it does far more than this role needs, and what it would supply comes from
# molecule-shared/playbook-context.yml instead.
- name: Include roles for matrix-bot-baibot Molecule tests
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: true
tasks:
- name: Include roles for matrix-bot-baibot Molecule tests
ansible.builtin.include_role:
name: "{{ role_name }}"
public: true
loop:
- com.devture.ansible.role.playbook_help
- com.devture.ansible.role.systemd_docker_base
- "custom/{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}"
loop_control:
loop_var: role_name

# The role installs the unit but does not start it - in the playbook that is
# `systemd_service_manager`'s job - so the scenario starts it here.
- name: Ensure matrix-bot-baibot is started
hosts: all
become: true
gather_facts: false
tasks:
- name: Ensure systemd daemon is reloaded
ansible.builtin.systemd_service:
daemon_reload: true

- name: Ensure matrix-bot-baibot systemd service is started
ansible.builtin.systemd_service:
name: matrix-bot-baibot.service
state: started

+ 93
- 0
roles/custom/matrix-bot-baibot/molecule/default/molecule.yml Прегледај датотеку

@@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
force: true
driver:
name: docker
platforms:
- name: matrix-bot-baibot-${MOLECULE_DISTRO:-ubuntu2604}-default
image: "geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2604}-ansible:latest"
command: ${MOLECULE_DOCKER_COMMAND:-""}
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
privileged: true
pre_build_image: true
provisioner:
name: ansible
config_options:
defaults:
callback_result_format: yaml
inventory:
group_vars:
all:
matrix_bot_baibot_container_network: matrix-bot-baibot-molecule

# verify.yml runs as its own play, where the role's defaults are out
# of scope, so the paths it reads are pinned here as literals. They
# match what the role derives from matrix_base_data_path.
matrix_bot_baibot_base_path: /matrix/baibot
matrix_bot_baibot_config_path: /matrix/baibot/config
matrix_bot_baibot_data_path: /matrix/baibot/data

# baibot is a plain Matrix client, not an appservice: it logs in with a
# password and then syncs, so a homeserver has to answer for it to get
# anywhere. prepare.yml stands up the shared stub for that.
matrix_bot_baibot_config_homeserver_url: http://matrix.molecule.local:8008

# Deliberately different from the role's defaults (localpart `baibot`,
# name `baibot`, prefix `!bai`, self-introduction on) AND from baibot's
# own built-in defaults, so that a passing assertion cannot be explained
# by "it would have happened anyway".
matrix_bot_baibot_config_user_mxid_localpart: molecule-baibot
matrix_bot_baibot_config_user_name: Molecule baibot
matrix_bot_baibot_config_user_password: molecule_baibot_password_5b7c14
matrix_bot_baibot_config_command_prefix: "!molecule-bai"
matrix_bot_baibot_config_room_post_join_self_introduction_enabled: false
matrix_bot_baibot_config_access_admin_patterns:
- "@molecule-admin:molecule.local"

# `debug` rather than the role's `info`, so the journal carries what the
# bot loaded. verify.yml reads it.
matrix_bot_baibot_config_logging_level_baibot: debug

# baibot talks to AI providers, and a scenario must not need a provider
# account. It does not have to: providers are contacted only when a
# message asks an agent to do something, never at startup. So a static
# agent is defined with a placeholder key and a base URL that resolves
# nowhere. Nothing is ever called, and the agent still has to survive
# baibot's startup parsing of `agents.static_definitions` - which is
# what proves the role's provider templating produced something the bot
# accepts.
matrix_bot_baibot_config_agents_static_definitions_anthropic_enabled: true
matrix_bot_baibot_config_agents_static_definitions_anthropic_id: molecule-anthropic
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_base_url: http://molecule-no-such-provider.invalid/v1
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_api_key: molecule-placeholder-not-a-real-key
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_model_id: molecule-model-4-2
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_temperature: 0.25
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_response_tokens: 1234
matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_context_tokens: 56789
env:
# Workaround for https://github.com/ansible/molecule/issues/4391
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/../..:/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:~/.ansible/roles
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- verify
- cleanup
- destroy
verifier:
name: ansible

+ 85
- 0
roles/custom/matrix-bot-baibot/molecule/default/prepare.yml Прегледај датотеку

@@ -0,0 +1,85 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Prepare matrix-bot-baibot Molecule tests
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: true
tasks:
- name: Ensure apt cache is updated
ansible.builtin.apt:
update_cache: true
cache_valid_time: 600
when: ansible_os_family == 'Debian'

- name: Ensure required packages are installed
ansible.builtin.package:
name:
- python3-requests
- fuse-overlayfs
state: present

- name: Ensure Docker is installed
ansible.builtin.include_role:
name: ansible-role-docker
vars:
docker_daemon_options:
storage-driver: fuse-overlayfs

# The role's file tasks set owner/group by name, and Ansible resolves those
# through the passwd database - so they have to exist before it runs. In a
# real deployment `matrix-base` creates them.
- name: Ensure the matrix group exists
ansible.builtin.group:
name: "{{ matrix_group_name }}"
gid: "{{ matrix_user_gid }}"
state: present

- name: Ensure the matrix user exists
ansible.builtin.user:
name: "{{ matrix_user_name }}"
uid: "{{ matrix_user_uid }}"
group: "{{ matrix_group_name }}"
create_home: false
system: true
state: present

- name: Ensure the base data path exists
ansible.builtin.file:
path: "{{ matrix_base_data_path }}"
state: directory
owner: "{{ matrix_user_name }}"
group: "{{ matrix_group_name }}"
mode: "0750"

- name: Ensure the container network the role attaches to exists
ansible.builtin.command:
argv:
- docker
- network
- create
- "{{ matrix_bot_baibot_container_network }}"
register: matrix_bot_baibot_molecule_network
changed_when: matrix_bot_baibot_molecule_network.rc == 0
failed_when:
- matrix_bot_baibot_molecule_network.rc != 0
- "'already exists' not in matrix_bot_baibot_molecule_network.stderr"

# baibot logs in and then syncs forever; with no homeserver answering it
# never gets past login and the unit crash-loops. The shared stub is enough
# - see molecule-shared/homeserver-stub.py for what it is not.
#
# It is told to claim the bot's own MXID, because the bot resolves who it is
# from what the homeserver hands back at login, and everything it does
# afterwards (its profile, its own-message filtering) hangs off that.
- name: Ensure the homeserver stub is running
ansible.builtin.include_tasks:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/tasks/homeserver-stub.yml"
vars:
molecule_shared_stub_network: "{{ matrix_bot_baibot_container_network }}"
molecule_shared_stub_user_id: "@{{ matrix_bot_baibot_config_user_mxid_localpart }}:{{ matrix_domain }}"

+ 1
- 0
roles/custom/matrix-bot-baibot/molecule/default/requirements.yml Прегледај датотеку

@@ -0,0 +1 @@
../../../../../molecule-shared/requirements.yml

+ 248
- 0
roles/custom/matrix-bot-baibot/molecule/default/verify.yml Прегледај датотеку

@@ -0,0 +1,248 @@
# SPDX-FileCopyrightText: 2026 Slavi Pantaleev
#
# SPDX-License-Identifier: AGPL-3.0-or-later

---
- name: Verify matrix-bot-baibot
hosts: all
become: true
vars_files:
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
- "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
gather_facts: false

tasks:
# The version is read out of the role's own defaults rather than pinned in
# molecule.yml, so that the assertion further down compares the running
# image against what defaults/main.yml actually ships. Pinning it here
# would make that assertion compare the scenario with itself.
- name: Load the role's defaults under a separate name
ansible.builtin.include_vars:
file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
name: matrix_bot_baibot_role_defaults

- name: Wait for the matrix-bot-baibot service to become active
ansible.builtin.systemd_service:
name: matrix-bot-baibot.service
register: matrix_bot_baibot_service
until: matrix_bot_baibot_service.status.ActiveState == 'active'
retries: 30
delay: 5
failed_when: false

# `Restart=always` means a crash-looping container still reports `active`,
# so the restart counter is checked alongside it. Asserted as `is defined`
# too, because `| int` turns a missing property into 0 and would pass
# vacuously on a systemd that does not expose it.
- name: Assert the service is active and has not been restarting
ansible.builtin.assert:
that:
- matrix_bot_baibot_service.status.ActiveState == 'active'
- matrix_bot_baibot_service.status.NRestarts is defined
- matrix_bot_baibot_service.status.NRestarts | int == 0
fail_msg: >-
matrix-bot-baibot.service is
{{ matrix_bot_baibot_service.status.ActiveState | default('unknown') }}
after {{ matrix_bot_baibot_service.status.NRestarts | default('?') }}
automatic restart(s)
success_msg: "matrix-bot-baibot.service is active and has not restarted"

# baibot publishes no port of its own - it is a Matrix client, not a server -
# so what it says about itself has to come from its output. The unit runs
# `docker start --attach`, so `--log-driver=none` on the container does not
# stop the journal from carrying it.
#
# `Syncing..` is the line that matters, and it is what carries this scenario
# rather than the unit check above. baibot does not exit when its startup
# goes wrong: a profile step it cannot complete is retried forever with a
# growing delay, so the unit stays `active` with `NRestarts` at 0 while the
# bot never reaches its message loop. Point the avatar at a file that is not
# there and the assertion above still passes; this one does not.
- name: Wait for baibot to reach its sync loop
ansible.builtin.shell:
cmd: >-
set -o pipefail && journalctl -u matrix-bot-baibot.service --no-pager -o cat
| sed -e 's/\x1b\[[0-9;]*m//g'
executable: /bin/bash
register: matrix_bot_baibot_journal
changed_when: false
until: "'Syncing..' in matrix_bot_baibot_journal.stdout"
retries: 24
delay: 5
failed_when: false

- name: Assert baibot got past startup and into its sync loop
ansible.builtin.assert:
that:
- "'Syncing..' in matrix_bot_baibot_journal.stdout"
- "'Failed to prepare profile' not in matrix_bot_baibot_journal.stdout"
fail_msg: >-
baibot never reached its sync loop; it is still in startup or stuck
retrying profile setup
success_msg: "baibot got past startup and is syncing"

# `user.name` is the bot's display name. The scenario's value is neither the
# role's default (`baibot`) nor what the stub reports the account already has
# (`stub`), so the bot naming this as what it wants can only have come from
# the configuration the role rendered.
- name: Assert the display name the role configured reached the process
ansible.builtin.assert:
that:
- >-
'desired_display_name="' ~ matrix_bot_baibot_config_user_name ~ '"'
in matrix_bot_baibot_journal.stdout
fail_msg: >-
baibot did not report {{ matrix_bot_baibot_config_user_name }} as the
display name it wants, so `user.name` did not reach the process
success_msg: "baibot acts on the display name the role configured"

# The `logging` setting is one string carrying per-target levels
# (`warn,mxlink=info,baibot=debug`), so proving it arrived means proving that
# different targets ended up at different levels - a single global level
# would satisfy neither half of this.
#
# First clause: baibot's own records appear at DEBUG, which the role's
# default of `info` would not produce.
#
# Second clause is the control, and it is not vacuous: at DEBUG the crates
# underneath (matrix-sdk and its spans, hyper, eyeball) are extremely
# talkative - raising the catch-all level turns these two records into
# roughly a hundred. Their silence is the `warn` catch-all being enforced.
#
# A control on mxlink was tried first and is the trap here: mxlink happens to
# emit no DEBUG records at all on a first run, so asserting their absence
# passed just as happily with mxlink set to `debug`.
- name: Assert the per-target logging levels reached the process
ansible.builtin.assert:
that:
- matrix_bot_baibot_debug_lines | select('search', 'baibot::') | list | length > 0
- matrix_bot_baibot_debug_lines | reject('search', 'baibot::') | list | length == 0
fail_msg: >-
The rendered `logging` string did not take effect:
{{ matrix_bot_baibot_debug_lines | length }} DEBUG record(s), of which
{{ matrix_bot_baibot_debug_lines | select('search', 'baibot::') | list | length }}
from baibot itself
success_msg: >-
baibot logs at DEBUG while everything under it stays at the catch-all
level, as the rendered `logging` string asks
vars:
matrix_bot_baibot_debug_lines: >-
{{ matrix_bot_baibot_journal.stdout_lines | select('search', ' DEBUG ') | list }}

- name: Read the configuration file the role rendered
ansible.builtin.slurp:
src: "{{ matrix_bot_baibot_config_path }}/config.yml"
register: matrix_bot_baibot_config_file

- name: Assert the rendered configuration carries this scenario's Matrix settings
ansible.builtin.assert:
that:
- matrix_bot_baibot_config.homeserver.server_name == matrix_domain
- matrix_bot_baibot_config.homeserver.url == matrix_bot_baibot_config_homeserver_url
- matrix_bot_baibot_config.user.mxid_localpart == matrix_bot_baibot_config_user_mxid_localpart
- matrix_bot_baibot_config.user.name == matrix_bot_baibot_config_user_name
- matrix_bot_baibot_config.command_prefix == matrix_bot_baibot_config_command_prefix
- matrix_bot_baibot_config.room.post_join_self_introduction_enabled is false
- matrix_bot_baibot_config.access.admin_patterns == matrix_bot_baibot_config_access_admin_patterns
- matrix_bot_baibot_config.initial_global_config.user_patterns == ['@*:' ~ matrix_domain]
fail_msg: "The rendered configuration does not carry the scenario's Matrix settings"
success_msg: "The rendered configuration carries the scenario's Matrix settings"
vars:
matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"

# The role supports two mutually-exclusive authentication modes and refuses
# a configuration that sets both. This scenario uses the password mode, so
# the access-token keys must be rendered as nulls rather than omitted or
# left with a value.
- name: Assert only the password authentication mode is rendered
ansible.builtin.assert:
that:
- matrix_bot_baibot_config.user.password == matrix_bot_baibot_config_user_password
- matrix_bot_baibot_config.user.access_token is none
- matrix_bot_baibot_config.user.device_id is none
fail_msg: "The rendered configuration does not use password authentication exclusively"
success_msg: "The rendered configuration uses password authentication exclusively"
vars:
matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"

# The agent presets are the most involved templating in this role: a
# per-provider template is rendered to YAML, parsed, merged with an
# extension, and dropped into the list as a nested structure. This asserts
# the whole round trip, key by key.
#
# No provider is ever contacted. baibot calls one only when a message asks an
# agent to do something, and the base URL here resolves nowhere on purpose -
# a scenario must not need an account with an AI provider.
- name: Assert the statically-defined agent survived the provider templating
ansible.builtin.assert:
that:
- matrix_bot_baibot_agents | length == 1
- matrix_bot_baibot_agent.id == matrix_bot_baibot_config_agents_static_definitions_anthropic_id
- matrix_bot_baibot_agent.provider == 'anthropic'
- matrix_bot_baibot_agent.config.base_url == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_base_url
- matrix_bot_baibot_agent.config.api_key == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_api_key
- matrix_bot_baibot_agent.config.text_generation.model_id == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_model_id
- matrix_bot_baibot_agent.config.text_generation.temperature == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_temperature
- matrix_bot_baibot_agent.config.text_generation.max_response_tokens == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_response_tokens
- matrix_bot_baibot_agent.config.text_generation.max_context_tokens == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_context_tokens
fail_msg: >-
The statically-defined agent is not what the role's preset variables ask
for: {{ matrix_bot_baibot_agents }}
success_msg: "The statically-defined agent carries the scenario's provider settings"
vars:
matrix_bot_baibot_agents: "{{ (matrix_bot_baibot_config_file.content | b64decode | from_yaml).agents.static_definitions }}"
matrix_bot_baibot_agent: "{{ matrix_bot_baibot_agents | first }}"

- name: Read the container's runtime configuration
ansible.builtin.command:
argv:
- docker
- container
- inspect
- matrix-bot-baibot
- --format
- "{{ '{{' }} .Config.Image {{ '}}' }} {{ '{{' }} .Config.User {{ '}}' }}"
register: matrix_bot_baibot_container
changed_when: false

- name: Assert the image carries the version defaults/main.yml pins
ansible.builtin.assert:
that:
- matrix_bot_baibot_role_defaults.matrix_bot_baibot_version in matrix_bot_baibot_container.stdout
fail_msg: >-
The running container is {{ matrix_bot_baibot_container.stdout }},
which does not carry the pinned version
{{ matrix_bot_baibot_role_defaults.matrix_bot_baibot_version }}
success_msg: "The running container is the version defaults/main.yml pins"

# The uid/gid come from outside the role (matrix-base supplies them in a real
# run, molecule-shared/playbook-context.yml here) and are deliberately not
# 1000, which the base image already uses - so this cannot pass by
# coincidence with whatever the image would have run as.
- name: Assert the container runs as the identity the playbook supplies
ansible.builtin.assert:
that:
- "matrix_user_uid ~ ':' ~ matrix_user_gid in matrix_bot_baibot_container.stdout"
fail_msg: >-
The container does not run as {{ matrix_user_uid }}:{{ matrix_user_gid }}
({{ matrix_bot_baibot_container.stdout }})
success_msg: "The container runs as the uid/gid the playbook supplies"

# baibot keeps its session and crypto store here. The file existing proves
# the bind mount is writable by the user the container runs as - a
# read-only-root container whose data directory it could not write would
# never have got as far as logging in.
- name: Stat the session file baibot persists
ansible.builtin.stat:
path: "{{ matrix_bot_baibot_data_path }}/session.json"
register: matrix_bot_baibot_session_file

- name: Assert baibot persisted its session as the matrix user
ansible.builtin.assert:
that:
- matrix_bot_baibot_session_file.stat.exists
- matrix_bot_baibot_session_file.stat.uid | int == matrix_user_uid | int
fail_msg: >-
{{ matrix_bot_baibot_data_path }}/session.json is missing or not owned
by uid {{ matrix_user_uid }}
success_msg: "baibot persisted its session into the data path as the matrix user"

Loading…
Откажи
Сачувај