mirror of
https://github.com/spantaleev/matrix-docker-ansible-deploy.git
synced 2026-11-07 06:04:18 +00:00
claude-sonnet-4-5 retires on 2026-11-30. baibot now uses Anthropic API version 2026-10-21, which rejects a temperature on every model, so matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_temperature is removed (and reported if still set). The default max response tokens go from 8192 to 16384, since thinking tokens count against them. Needs the baibot release that moves to API version 2026-10-21. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
12 KiB
YAML
231 lines
12 KiB
YAML
# 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:
|
|
# Read from the role's own defaults rather than pinned in molecule.yml, so the version
|
|
# assertion compares the running image against what defaults/main.yml 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 too. Asserted `is defined` because `| int` turns a missing property
|
|
# into 0 and would pass vacuously.
|
|
- 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 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` does not stop
|
|
# the journal from carrying it.
|
|
#
|
|
# `Syncing..` is what carries this scenario, not the unit check above. baibot does not exit
|
|
# when 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 missing file 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"
|
|
|
|
# The scenario's display name is neither the role's default nor what the stub reports the
|
|
# account already has, so the bot wanting it can only have come from what 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"
|
|
|
|
# `logging` is one string carrying per-target levels, so proving it arrived means proving
|
|
# different targets ended up at different levels. A single global level satisfies neither half.
|
|
#
|
|
# 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 are
|
|
# extremely talkative, so raising the catch-all turns these two records into roughly a
|
|
# hundred. Their silence is the `warn` catch-all being enforced.
|
|
#
|
|
# The trap here: a control on mxlink was tried first, and mxlink emits 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 refuses a configuration that sets both authentication modes. This scenario uses
|
|
# password mode, so the access-token keys must render as nulls, not be omitted or set.
|
|
- 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
|
|
# rendered to YAML, parsed, merged with an extension, nested into the list. Asserted as a
|
|
# 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.
|
|
- 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.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 and are deliberately not 1000, which the base
|
|
# image already uses, so this cannot pass by coinciding with the image's own user.
|
|
- 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.
|
|
- 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"
|