#!/usr/bin/env bats load test_helper # The mDNS helper and the configure items built on it (ai-servers, storage), # with fake avahi-browse, curl and smbclient. Nothing here touches the network. setup() { common_setup export COREDESKTOP="$REPO" AVAHI="$BATS_TEST_TMPDIR/avahi" # What avahi-browse --parsable prints, including this machine (basement), # an IPv6 copy of a service, and an unresolved (+) line cat > "$AVAHI" << 'EOF' +;enp5s0;IPv4;llm;_ssh._tcp;local =;enp5s0;IPv4;llm;_ssh._tcp;local;llm.local;192.168.0.5;22; =;enp5s0;IPv6;llm;_ssh._tcp;local;llm.local;fe80::1;22; =;enp5s0;IPv4;llm;_sftp-ssh._tcp;local;llm.local;192.168.0.5;22; =;enp5s0;IPv4;basement;_ssh._tcp;local;basement.local;192.168.0.106;22; =;enp5s0;IPv4;PI-STAR;_smb._tcp;local;pi-star.local;192.168.0.102;445; =;enp5s0;IPv4;HP\032LaserJet\032MFP\032M234dw\032\0403BE90A\041\032\064\032lab;_ipp._tcp;local;lab.local;192.168.0.22;631;"rp=printers/HP" EOF export AVAHI stub avahi-browse 'cat "$AVAHI"' stub hostnamectl 'echo basement' confirm_yes() { return 0; } confirm() { return 0; } unsandboxed() { echo "unsandboxed $*" >> "$STUB_LOG"; } export -f confirm_yes confirm unsandboxed } # --- lib/mdns.sh -------------------------------------------------------------------- @test "mdns_unescape decodes Avahi's decimal escapes" { source "$REPO/configure.d/lib/mdns.sh" [ "$(mdns_unescape 'HP\032LaserJet\032\0403BE90A\041\032\064\032lab')" = "HP LaserJet (3BE90A) @ lab" ] [ "$(mdns_unescape 'a\.b')" = "a.b" ] [ "$(mdns_unescape plain)" = "plain" ] } @test "mdns_services lists each resolved IPv4 service once, with its name decoded" { source "$REPO/configure.d/lib/mdns.sh" run mdns_services [ "$status" -eq 0 ] [ "${#lines[@]}" -eq 5 ] [[ "$output" == *"HP LaserJet MFP M234dw (3BE90A) @ lab|_ipp._tcp|lab.local|192.168.0.22|631|\"rp=printers/HP\""* ]] [[ "$output" != *"fe80"* ]] } @test "mdns_hosts lists the other machines, not this one" { source "$REPO/configure.d/lib/mdns.sh" run mdns_hosts [ "$status" -eq 0 ] [ "$output" = "$(printf 'lab.local\nllm.local\npi-star.local')" ] } # --- configure ai-servers -------------------------------------------------------------- setup_ai() { source "$REPO/configure.d/50-ai-servers.sh" # llm.local:11434 is Ollama with two models; nothing else answers stub curl 'case "$*" in *http://llm.local:11434/v1/models*) echo "{\"data\": [{\"id\": \"qwen3-coder:30b\"}, {\"id\": \"gemma4:12b\"}]}" ;; *http://llm.local:11434/api/version*) echo "{\"version\": \"0.14.0\"}" ;; *) exit 7 ;; esac' } @test "find_servers asks each machine on the LLM ports and lists the ones that answer" { setup_ai run find_servers [ "$status" -eq 0 ] [ "$output" = "llm.local:11434|2" ] [ "$(server_kind llm.local:11434)" = Ollama ] } @test "sandbox_allow adds a server once, keeping what's there" { setup_ai mkdir -p "$(dirname "$SANDBOX_CONF")" echo '{"network": {"allowedDomains": ["docs.example.com"]}, "filesystem": {"allowWrite": ["~/notes"]}}' > "$SANDBOX_CONF" sandbox_allow llm.local:11434 sandbox_allow llm.local:11434 [ "$(jq -c '.network.allowedDomains' "$SANDBOX_CONF")" = '["docs.example.com","llm.local:11434"]' ] [ "$(jq -c '.filesystem.allowWrite' "$SANDBOX_CONF")" = '["~/notes"]' ] sandbox_disallow llm.local:11434 [ "$(jq -c '.network.allowedDomains' "$SANDBOX_CONF")" = '["docs.example.com"]' ] } @test "sandbox_allow leaves a file that isn't valid JSON alone" { setup_ai mkdir -p "$(dirname "$SANDBOX_CONF")" echo 'not json' > "$SANDBOX_CONF" run sandbox_allow llm.local:11434 [ "$status" -ne 0 ] [ "$(cat "$SANDBOX_CONF")" = "not json" ] } @test "claude_allow starts from the default domains when Claude has no list, and keeps their order" { setup_ai mkdir -p "$HOME/.claude" echo '{"model": "opus"}' > "$CLAUDE_SETTINGS" claude_allow llm.local:11434 [ "$(jq -c '.sandbox.network.allowedDomains[0:2]' "$CLAUDE_SETTINGS")" = '["anthropic.com","*.anthropic.com"]' ] [ "$(jq -r '.sandbox.network.allowedDomains[-1]' "$CLAUDE_SETTINGS")" = llm.local:11434 ] [ "$(jq -r '.model' "$CLAUDE_SETTINGS")" = opus ] claude_disallow llm.local:11434 run jq -e '.sandbox.network.allowedDomains | index("llm.local:11434")' "$CLAUDE_SETTINGS" [ "$status" -ne 0 ] } @test "claude_allow does nothing without Claude's settings" { setup_ai claude_allow llm.local:11434 [ ! -e "$CLAUDE_SETTINGS" ] } @test "opencode_add adds the server's models as a provider, and opencode_remove takes it out" { setup_ai opencode_add llm.local:11434 Ollama gemma4:12b > /dev/null [ "$(opencode_provider llm.local:11434)" = llm-11434 ] [ "$(jq -r '.provider["llm-11434"].options.baseURL' "$OPENCODE_CONF")" = "http://llm.local:11434/v1" ] [ "$(jq -r '.provider["llm-11434"].name' "$OPENCODE_CONF")" = "Ollama on llm.local" ] [ "$(jq -c '.provider["llm-11434"].models | keys' "$OPENCODE_CONF")" = '["gemma4:12b","qwen3-coder:30b"]' ] [ "$(jq -r '.model' "$OPENCODE_CONF")" = "llm-11434/gemma4:12b" ] opencode_remove llm.local:11434 [ "$(jq -c '.provider' "$OPENCODE_CONF")" = '{}' ] [ "$(jq -r '.model // "none"' "$OPENCODE_CONF")" = none ] } @test "hermes_use points Hermes at the server's OpenAI API" { setup_ai hermes_use llm.local:11434 gemma4:12b called unsandboxed hermes config set model.provider custom called unsandboxed hermes config set model.base_url http://llm.local:11434/v1 called unsandboxed hermes config set model.default gemma4:12b } @test "add_server allows the server and remembers it; remove_server undoes it" { setup_ai mkdir -p "$(dirname "$AI_SERVERS_CONF")" touch "$AI_SERVERS_CONF" stub systemctl 'exit 3' run add_server llm.local:11434 <<< "2" [ "$status" -eq 0 ] [ "$(cat "$AI_SERVERS_CONF")" = llm.local:11434 ] jq -e '.network.allowedDomains | index("llm.local:11434")' "$SANDBOX_CONF" run remove_server llm.local:11434 [ "$status" -eq 0 ] [ ! -s "$AI_SERVERS_CONF" ] run jq -e '.network.allowedDomains | index("llm.local:11434")' "$SANDBOX_CONF" [ "$status" -ne 0 ] } @test "add_server refuses an address that isn't an LLM server" { setup_ai run add_server lab.local:11434 <<< "1" [ "$status" -eq 1 ] [[ "$output" == *"doesn't answer like an LLM server"* ]] [ ! -e "$SANDBOX_CONF" ] } # --- configure storage ------------------------------------------------------------------- @test "find_storage lists SMB and SFTP servers, not this machine" { source "$REPO/configure.d/60-storage.sh" run find_storage [ "$status" -eq 0 ] [ "$output" = "$(printf 'sftp|llm.local|llm\nsmb|pi-star.local|PI-STAR')" ] } @test "smb_shares lists disk shares, without admin shares, and passes the password outside the arguments" { source "$REPO/configure.d/60-storage.sh" stub smbclient 'echo "PASSWD=$PASSWD" >> "$STUB_LOG" printf "Disk|files|Shared files\nDisk|C\$|Default share\nIPC|IPC\$|IPC Service\nDisk|My Music|\n"' run smb_shares nas.local me secret [ "$output" = "$(printf 'files\nMy Music')" ] called smbclient -L //nas.local -U me -g called PASSWD=secret } @test "smb_fstab_line mounts as you on first use, with \\040 for spaces" { source "$REPO/configure.d/60-storage.sh" run smb_fstab_line "//nas.local/My Files" /mnt/nas-my-files /etc/coredesktop/smb/nas-my-files [[ "$output" == "//nas.local/My\\040Files /mnt/nas-my-files cifs credentials=/etc/coredesktop/smb/nas-my-files,uid=$(id -u),gid=$(id -g),"* ]] [[ "$output" == *",x-systemd.automount,"*" 0 0" ]] run smb_fstab_line //nas.local/pub /mnt/nas-pub "" [[ "$output" == *" cifs guest,"* ]] } @test "mount_point names the folder after the host and share, avoiding ones in use" { source "$REPO/configure.d/60-storage.sh" [ "$(mount_point Bats-Test-NAS.local 'My Files')" = /mnt/bats-test-nas-my-files ] [ "$(mount_point bats-test-host.local '')" = /mnt/bats-test-host ] mkdir -p "$(dirname "$STORAGE_CONF")" echo "sftp|me@bats-test-host.local:|/mnt/bats-test-host" > "$STORAGE_CONF" [ "$(mount_point bats-test-host.local '')" = /mnt/bats-test-host-2 ] } @test "sftp_unit runs sftp-mount as a user service" { source "$REPO/configure.d/60-storage.sh" run sftp_unit "me@brain.local:/srv/my data" /mnt/brain-my-data [[ "$output" == *'ExecStart=%h/.local/share/coredesktop/system/sftp-mount "me@brain.local:/srv/my data" "/mnt/brain-my-data"'* ]] [[ "$output" == *"WantedBy=default.target"* ]] [ "$(sftp_unit_name /mnt/brain-my-data)" = coredesktop-sftp-brain-my-data.service ] } @test "enter_and_add reads smb:// and sftp:// addresses" { source "$REPO/configure.d/60-storage.sh" add_smb() { echo "smb [$1] [$2]"; } add_sftp() { echo "sftp [$1] [$2] [$3]"; } [ "$(enter_and_add <<< 'smb://nas.local/My Files' | tail -n1)" = "smb [nas.local] [My Files]" ] [ "$(enter_and_add <<< 'sftp://brain.local' | tail -n1)" = "sftp [brain.local] [] []" ] [ "$(enter_and_add <<< 'sftp://me@brain.local/srv/data' | tail -n1)" = "sftp [brain.local] [me] [/srv/data]" ] run enter_and_add <<< 'smb://nas.local' [ "$status" -eq 1 ] }