--- # Offline regression tests for the role's Jinja expressions, run against # fixtures captured from a real `multica` CLI. No target host required: # # ansible-playbook -i localhost, tests/expressions.yml # # This does NOT replace the scratch-VM run described in the role README; it # catches the class of bug where a filter chain silently returns the wrong # type or quietly matches nothing. - name: Expressions | parsing real multica CLI output hosts: localhost gather_facts: false vars: multica_server_url: "https://multica.wdw-labs.cloud" multica_app_url: "" multica_workspaces_root: "/srv/multica/workspaces" multica_device_name: "rblundon--Mac16" multica_runtime_name: "" multica_max_concurrent_tasks: "" multica_poll_interval: "" multica_heartbeat_interval: "" multica_agent_timeout: "" multica_disable_auto_update: true multica_version: "0.6.5" multica_agent_clis: [claude, hermes] multica_agent_cli_catalog: claude: {provider: claude, method: npm} hermes: {provider: hermes, method: script} cursor: {provider: cursor, method: npm} _multica_arch_map: {x86_64: amd64, aarch64: arm64} tasks: - name: Load fixtures ansible.builtin.set_fact: rt: "{{ lookup('file', '{{ playbook_dir }}/fixtures/runtime-list.json') }}" ds: "{{ lookup('file', '{{ playbook_dir }}/fixtures/daemon-status.json') }}" cs_lines: "{{ lookup('file', '{{ playbook_dir }}/fixtures/config-show.txt').splitlines() }}" ver_out: "multica 0.6.5 (commit: 654b8ca95, built: 2026-10-21T09:41:38Z)" # --- CLI version extraction (multica_cli.yml) --------------------------- - name: Extract installed CLI version ansible.builtin.set_fact: found: >- {{ ((([ver_out] | first | default('')).split() + ['', ''])[1]) }} - name: Assert version extraction ansible.builtin.assert: that: [(found | trim) == multica_version] success_msg: "CLI version extraction OK -> {{ found }}" # --- config show parsing (config.yml) ----------------------------------- - name: Parse config show ansible.builtin.set_fact: current: >- {{ dict( cs_lines | select('match', '^[a-z_]+:') | map('regex_replace', '^([a-z_]+):\s*(.*?)\s*$', '\1|\2') | map('split', '|') ) }} - name: Build desired config ansible.builtin.set_fact: desired: >- {{ { 'server_url': multica_server_url, 'app_url': multica_app_url, 'workspaces_root': multica_workspaces_root, 'device_name': multica_device_name, 'runtime_name': multica_runtime_name, 'max_concurrent_tasks': multica_max_concurrent_tasks, 'poll_interval': multica_poll_interval, 'heartbeat_interval': multica_heartbeat_interval, 'agent_timeout': multica_agent_timeout, 'disable_auto_update': ('true' if multica_disable_auto_update | bool else 'false'), } | dict2items | rejectattr('value', 'equalto', '') | list | items2dict }} - name: Show parse results ansible.builtin.debug: msg: parsed_keys: "{{ current.keys() | list }}" disable_auto_update_now: "{{ current['disable_auto_update'] | default('MISSING') }}" server_url_now: "{{ current['server_url'] | default('MISSING') }}" desired: "{{ desired }}" would_change: >- {{ desired | dict2items | selectattr('key', 'in', current.keys() | list) | rejectattr('value', 'equalto', 'IMPOSSIBLE') | map(attribute='key') | list }} - name: Assert config parsing found the expected keys ansible.builtin.assert: that: - "'server_url' in current" - "'disable_auto_update' in current" - current['server_url'] == '(not set)' - current['disable_auto_update'] == 'false' - desired | length == 4 - "'app_url' not in desired" success_msg: "config show parsing + desired-config filtering OK" # --- runtime verification (verify.yml) ---------------------------------- - name: Compute online providers for this daemon ansible.builtin.set_fact: expected: "{{ multica_agent_clis | map('extract', multica_agent_cli_catalog, 'provider') | list }}" online: >- {{ rt | from_json | selectattr('daemon_id', 'equalto', (ds | from_json).daemon_id) | selectattr('status', 'equalto', 'online') | map(attribute='provider') | list }} - name: Assert every expected provider is online ansible.builtin.assert: that: [(expected | difference(online)) | length == 0] success_msg: "runtime check OK: expected={{ expected }} online={{ online }}" - name: Assert device_info naming check ansible.builtin.assert: that: - rt | from_json | selectattr('daemon_id', 'equalto', (ds | from_json).daemon_id) | rejectattr('device_info', 'search', '^' ~ (multica_device_name | regex_escape)) | list | length == 0 success_msg: "device_info check OK for {{ multica_device_name }}" - name: Assert daemon status fields ansible.builtin.assert: that: - (ds | from_json).status == 'running' - (ds | from_json).cli_version == multica_version - ((ds | from_json).server_url | regex_replace('/$','')) == (multica_server_url | regex_replace('/$','')) success_msg: "daemon status assertions OK" # --- negative case: a provider that is NOT online ------------------------ - name: Assert a missing provider is detected ansible.builtin.assert: that: - (['codex'] | difference(online)) | length == 1 success_msg: "missing-provider detection OK" - name: Expressions | preflight fact-building hosts: localhost gather_facts: false vars: multica_install_dir: /usr/local/bin multica_agent_cli_catalog: claude: {provider: claude, method: npm} hermes: {provider: hermes, method: script} multica_release_download_base_url: "https://github.com/multica-ai/multica/releases/download" multica_version: "0.6.5" _multica_arch_map: {x86_64: amd64, aarch64: arm64} tasks: - name: Case - profile unset, npm CLI selected ansible.builtin.set_fact: multica_profile: "" multica_agent_clis: [claude, hermes] _multica_arch: "{{ _multica_arch_map['x86_64'] }}" - name: Build facts ansible.builtin.set_fact: _multica_cmd: >- {{ [multica_install_dir ~ '/multica'] + (['--profile', multica_profile] if multica_profile | length > 0 else []) }} _multica_node_required: >- {{ multica_agent_clis | map('extract', multica_agent_cli_catalog, 'method') | select('equalto', 'npm') | list | length > 0 }} _multica_release_url: >- {{ multica_release_download_base_url }}/v{{ multica_version }}/multica-cli-{{ multica_version }}-linux-{{ _multica_arch }}.tar.gz - name: Assert types and values ansible.builtin.assert: that: - _multica_cmd is sequence - _multica_cmd is not string - _multica_cmd == ['/usr/local/bin/multica'] - (_multica_cmd + ['daemon', 'status']) == ['/usr/local/bin/multica', 'daemon', 'status'] - _multica_node_required | bool - _multica_release_url == 'https://github.com/multica-ai/multica/releases/download/v0.6.5/multica-cli-0.6.5-linux-amd64.tar.gz' success_msg: "argv prefix + node detection + URL OK: {{ _multica_cmd }}" - name: Case - profile set, script-only CLI selected ansible.builtin.set_fact: multica_profile: "haunted" multica_agent_clis: [hermes] - name: Rebuild facts ansible.builtin.set_fact: _multica_cmd: >- {{ [multica_install_dir ~ '/multica'] + (['--profile', multica_profile] if multica_profile | length > 0 else []) }} _multica_node_required: >- {{ multica_agent_clis | map('extract', multica_agent_cli_catalog, 'method') | select('equalto', 'npm') | list | length > 0 }} - name: Assert profile + no-node case ansible.builtin.assert: that: - _multica_cmd == ['/usr/local/bin/multica', '--profile', 'haunted'] - not (_multica_node_required | bool) success_msg: "profile prefix + node-not-required OK: {{ _multica_cmd }}" - name: Assert PAT prefix validation accepts both token kinds and rejects junk ansible.builtin.assert: that: - "'mul_abc123' is match('^(mul_|mcn_)')" - "'mcn_abc123' is match('^(mul_|mcn_)')" - "not ('ghp_abc123' is match('^(mul_|mcn_)'))" - "not ('' is match('^(mul_|mcn_)'))" success_msg: "PAT validation OK" - name: Assert unknown-agent detection ansible.builtin.assert: that: - "['claude', 'bogus'] | difference(multica_agent_cli_catalog.keys() | list) == ['bogus']" - "['claude', 'hermes'] | difference(multica_agent_cli_catalog.keys() | list) | length == 0" success_msg: "catalog validation OK" - name: Assert workspaces-root-under-mount logic vars: root: "/mnt/utilidor/multica/workspaces" mnt: "/mnt/utilidor" bad_root: "/mnt/utilidor-other/workspaces" ansible.builtin.assert: that: - root == mnt or root.startswith(mnt.rstrip('/') ~ '/') - not (bad_root == mnt or bad_root.startswith(mnt.rstrip('/') ~ '/')) success_msg: "mount containment check OK (rejects /mnt/utilidor-other)" - name: Expressions | pinned agent-CLI version guard hosts: localhost gather_facts: false vars: # Mirrors the expression in tasks/agent_cli/npm.yml. guard: >- {{ rc != 0 or (pin | default('') | length > 0 and not ((out | default('')) is search( '(^|[^0-9.])' ~ (pin | regex_escape) ~ '([^0-9.]|$)'))) }} tasks: - name: Pinned version already installed - must NOT reinstall ansible.builtin.assert: that: [not (guard | bool)] success_msg: "2.1.304 pin vs '2.1.304 (Claude Code)' -> no install" vars: {rc: 0, pin: "2.1.304", out: "2.1.304 (Claude Code)"} - name: Different version installed - must reinstall ansible.builtin.assert: that: [guard | bool] success_msg: "2.1.304 pin vs 2.1.306 installed -> install" vars: {rc: 0, pin: "2.1.304", out: "2.1.306 (Claude Code)"} - name: Longer version must not satisfy a shorter pin ansible.builtin.assert: that: [guard | bool] success_msg: "2.1.304 pin vs 2.1.3045 installed -> install (no substring false-match)" vars: {rc: 0, pin: "2.1.304", out: "2.1.3045 (Claude Code)"} - name: Prefix of installed version must not satisfy the pin ansible.builtin.assert: that: [guard | bool] success_msg: "2.1.30 pin vs 2.1.304 installed -> install" vars: {rc: 0, pin: "2.1.30", out: "2.1.304 (Claude Code)"} - name: Version embedded mid-string still matches (hermes-style output) ansible.builtin.assert: that: [not (guard | bool)] success_msg: "0.21.5 pin vs 'Hermes Agent v0.21.5+2024.gb609c74' -> no install" vars: {rc: 0, pin: "0.21.5", out: "Hermes Agent v0.21.5+2024.gb609c74 (2026.9.24)"} - name: Binary absent - must install regardless of pin ansible.builtin.assert: that: [guard | bool] success_msg: "rc!=0 -> install" vars: {rc: 1, pin: "2.1.304", out: ""} - name: Unpinned tracks latest - never reinstalls once present ansible.builtin.assert: that: [not (guard | bool)] success_msg: "empty pin + present -> no install" vars: {rc: 0, pin: "", out: "2.1.306 (Claude Code)"}