|
- # 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"
|