added multica agent hosts
This commit is contained in:
1 parent
5559cd425d
commit
01cd7b8d1e
32 files changed
+2636
-417
No files matched your search
@@ -0,0 +1,275 @@
|
||||
---
|
||||
# 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)"}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
Config file: /home/multica/.config/multica/config.json
|
||||
server_url: (not set)
|
||||
app_url: (not set)
|
||||
workspace_id: (not set)
|
||||
device_name: (not set)
|
||||
runtime_name: (not set)
|
||||
workspaces_root: (not set)
|
||||
max_concurrent_tasks: (not set)
|
||||
poll_interval: (not set)
|
||||
ws_claim_poll_interval: (not set)
|
||||
heartbeat_interval: (not set)
|
||||
agent_timeout: (not set)
|
||||
codex_semantic_inactivity_timeout: (not set)
|
||||
codex_handshake_timeout: (not set)
|
||||
disable_auto_update: false
|
||||
auto_update_check_interval: (not set)
|
||||
disable_auto_reload: false
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"active_task_count": 1,
|
||||
"agents": [
|
||||
"claude",
|
||||
"opencode",
|
||||
"hermes",
|
||||
"cursor"
|
||||
],
|
||||
"cli_commit": "654b8ca954034f8181eb6bbb4fc298dede760156",
|
||||
"cli_version": "0.6.5",
|
||||
"daemon_id": "01a15ad9-f27e-72da-a1a5-53ec15b53f9d",
|
||||
"device_name": "rblundon--Mac16",
|
||||
"failed_terminal_report_bytes": 0,
|
||||
"failed_terminal_report_count": 0,
|
||||
"launched_by": "desktop",
|
||||
"os": "darwin",
|
||||
"pending_terminal_report_bytes": 0,
|
||||
"pending_terminal_report_count": 0,
|
||||
"pid": 34832,
|
||||
"profile": "desktop-multica.wdw-labs.cloud",
|
||||
"resource_wait_task_count": 0,
|
||||
"running_task_count": 1,
|
||||
"server_url": "https://multica.wdw-labs.cloud",
|
||||
"status": "running",
|
||||
"uptime": "4h12m4s",
|
||||
"workspaces": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000001",
|
||||
"runtimes": [
|
||||
"ea210ad4-b0e8-4663-a212-7d6878668ec7",
|
||||
"eb68ea3f-78d6-4214-974d-9022aff55216",
|
||||
"245f271d-339b-47d5-8ea6-1e7eb6576ece",
|
||||
"c8316de7-5668-4276-8b31-3e7f873b8499"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
[
|
||||
{
|
||||
"created_at": "2026-10-20T19:56:21Z",
|
||||
"custom_name": null,
|
||||
"daemon_id": "01a15ad9-f27e-72da-a1a5-53ec15b53f9d",
|
||||
"device_info": "rblundon--Mac16 \u00b7 2.1.274 (Claude Code)",
|
||||
"id": "ea210ad4-b0e8-4663-a212-7d6878668ec7",
|
||||
"last_seen_at": "2026-10-21T19:47:35Z",
|
||||
"launch_header": "claude (stream-json)",
|
||||
"metadata": {
|
||||
"capabilities": [
|
||||
"skill-bundles-v1",
|
||||
"coalesced-comments-v1",
|
||||
"execution-manifest-v1",
|
||||
"agent-skill-v1",
|
||||
"remote-mcp-v1",
|
||||
"local-worktree-v1",
|
||||
"source_context_quick_create_v1",
|
||||
"rpc-v1",
|
||||
"platform-skill-v1",
|
||||
"checkout-keeps-work-v1",
|
||||
"joined-wakeups-v1"
|
||||
],
|
||||
"cli_version": "0.6.5",
|
||||
"launched_by": "desktop",
|
||||
"version": "2.1.274 (Claude Code)"
|
||||
},
|
||||
"name": "Claude (rblundon--Mac16)",
|
||||
"owner_id": "00000000-0000-0000-0000-000000000000",
|
||||
"profile_id": null,
|
||||
"provider": "claude",
|
||||
"runtime_mode": "local",
|
||||
"status": "online",
|
||||
"updated_at": "2026-10-21T17:09:10Z",
|
||||
"visibility": "private",
|
||||
"workspace_id": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-10-20T19:56:22Z",
|
||||
"custom_name": null,
|
||||
"daemon_id": "01a15ad9-f27e-72da-a1a5-53ec15b53f9d",
|
||||
"device_info": "rblundon--Mac16 \u00b7 2025.09.18-7ae6800",
|
||||
"id": "eb68ea3f-78d6-4214-974d-9022aff55216",
|
||||
"last_seen_at": "2026-10-21T19:47:35Z",
|
||||
"launch_header": "cursor-agent (stream-json)",
|
||||
"metadata": {
|
||||
"capabilities": [
|
||||
"skill-bundles-v1",
|
||||
"coalesced-comments-v1",
|
||||
"execution-manifest-v1",
|
||||
"agent-skill-v1",
|
||||
"remote-mcp-v1",
|
||||
"local-worktree-v1",
|
||||
"source_context_quick_create_v1",
|
||||
"rpc-v1",
|
||||
"platform-skill-v1",
|
||||
"checkout-keeps-work-v1",
|
||||
"joined-wakeups-v1"
|
||||
],
|
||||
"cli_version": "0.6.5",
|
||||
"launched_by": "desktop",
|
||||
"version": "2025.09.18-7ae6800"
|
||||
},
|
||||
"name": "Cursor (rblundon--Mac16)",
|
||||
"owner_id": "00000000-0000-0000-0000-000000000000",
|
||||
"profile_id": null,
|
||||
"provider": "cursor",
|
||||
"runtime_mode": "local",
|
||||
"status": "online",
|
||||
"updated_at": "2026-10-21T17:09:10Z",
|
||||
"visibility": "private",
|
||||
"workspace_id": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-10-20T19:56:22Z",
|
||||
"custom_name": null,
|
||||
"daemon_id": "01a15ad9-f27e-72da-a1a5-53ec15b53f9d",
|
||||
"device_info": "rblundon--Mac16 \u00b7 Hermes Agent v0.21.5+2024.gb609c74 (2026.9.24) \u00b7 upstream f1247d2e",
|
||||
"id": "245f271d-339b-47d5-8ea6-1e7eb6576ece",
|
||||
"last_seen_at": "2026-10-21T19:47:35Z",
|
||||
"launch_header": "hermes acp",
|
||||
"metadata": {
|
||||
"capabilities": [
|
||||
"skill-bundles-v1",
|
||||
"coalesced-comments-v1",
|
||||
"execution-manifest-v1",
|
||||
"agent-skill-v1",
|
||||
"remote-mcp-v1",
|
||||
"local-worktree-v1",
|
||||
"source_context_quick_create_v1",
|
||||
"rpc-v1",
|
||||
"platform-skill-v1",
|
||||
"checkout-keeps-work-v1",
|
||||
"joined-wakeups-v1"
|
||||
],
|
||||
"cli_version": "0.6.5",
|
||||
"launched_by": "desktop",
|
||||
"version": "Hermes Agent v0.21.5+2024.gb609c74 (2026.9.24) \u00b7 upstream f1247d2e"
|
||||
},
|
||||
"name": "Hermes (rblundon--Mac16)",
|
||||
"owner_id": "00000000-0000-0000-0000-000000000000",
|
||||
"profile_id": null,
|
||||
"provider": "hermes",
|
||||
"runtime_mode": "local",
|
||||
"status": "online",
|
||||
"updated_at": "2026-10-21T17:09:10Z",
|
||||
"visibility": "private",
|
||||
"workspace_id": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-10-20T19:56:22Z",
|
||||
"custom_name": null,
|
||||
"daemon_id": "01a15ad9-f27e-72da-a1a5-53ec15b53f9d",
|
||||
"device_info": "rblundon--Mac16 \u00b7 1.18.20",
|
||||
"id": "c8316de7-5668-4276-8b31-3e7f873b8499",
|
||||
"last_seen_at": "2026-10-21T19:47:35Z",
|
||||
"launch_header": "opencode run (json)",
|
||||
"metadata": {
|
||||
"capabilities": [
|
||||
"skill-bundles-v1",
|
||||
"coalesced-comments-v1",
|
||||
"execution-manifest-v1",
|
||||
"agent-skill-v1",
|
||||
"remote-mcp-v1",
|
||||
"local-worktree-v1",
|
||||
"source_context_quick_create_v1",
|
||||
"rpc-v1",
|
||||
"platform-skill-v1",
|
||||
"checkout-keeps-work-v1",
|
||||
"joined-wakeups-v1"
|
||||
],
|
||||
"cli_version": "0.6.5",
|
||||
"launched_by": "desktop",
|
||||
"version": "1.18.20"
|
||||
},
|
||||
"name": "Opencode (rblundon--Mac16)",
|
||||
"owner_id": "00000000-0000-0000-0000-000000000000",
|
||||
"profile_id": null,
|
||||
"provider": "opencode",
|
||||
"runtime_mode": "local",
|
||||
"status": "online",
|
||||
"updated_at": "2026-10-21T17:09:10Z",
|
||||
"visibility": "private",
|
||||
"workspace_id": "00000000-0000-0000-0000-000000000001"
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user