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