Files
pulse/scripts/installtests/install_sh_test.go
T
rcourtman f8c5c55d11 Let --report-ip lead host identity and pass it through the installer
An explicit --report-ip is the user naming the primary address on a
multi-NIC host, but identityFromHost appended it after the auto-detected
interface addresses while every consumer of ResourceIdentity.IPAddresses
treats the first entry as primary, so the override never changed what
the Machines table displayed. Prepend it instead.

The install script also rejected --report-ip as an unknown argument even
though the agent supports the flag, forcing hand edits to the service
unit that a later --update run would drop. Accept the flag, render it
into the service ExecStart, persist it in connection state, and
recognise it during saved-state and arg-stream recovery so updates
preserve it.

Refs #829

Contract-Neutral: behavioral fix: user-specified report-ip leads host identity addresses and the installer passes --report-ip through; no public contract change (#829)
2026-07-22 00:49:43 +01:00

4369 lines
143 KiB
Go

package installtests
import (
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
)
func TestInstallSHAllowsMissingTokenForOptionalAuth(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`build_exec_arg_items() {`,
`RUNTIME_TOKEN_FILE=""`,
`EXEC_ARG_ITEMS+=(--token-file "$RUNTIME_TOKEN_FILE")`,
`fail "Internal installer error: runtime token file was not prepared before service rendering." "$EXIT_GENERAL"`,
`build_exec_args_without_token() {`,
`build_exec_arg_items "false"`,
`build_exec_arg_items "true"`,
`if [[ -n "$PULSE_TOKEN" && ! "$PULSE_TOKEN" =~ ^[a-fA-F0-9]+$ ]]; then`,
`if [[ -n "$PULSE_TOKEN" ]]; then`,
`log_info "No API token provided; installer will configure token-optional agent runtime."`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing optional-token handling: %s", needle)
}
}
}
func TestInstallSHPersistsAndVerifiesServerFingerprint(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}"`,
`--server-fingerprint <sha256>`,
`verify_pinned_server_certificate() {`,
`openssl s_client -connect "$target" -servername "$host"`,
`EXEC_ARG_ITEMS+=(--server-fingerprint "$SERVER_FINGERPRINT")`,
`write_connection_state_value "$conn_tmp" "PULSE_SERVER_FINGERPRINT" "$SERVER_FINGERPRINT"`,
`SERVER_FINGERPRINT=$(read_connection_state_value "$file" "PULSE_SERVER_FINGERPRINT")`,
`if [[ "$OS" == "linux" || "$OS" == "freebsd" ]]; then`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing server-fingerprint lifecycle contract: %s", needle)
}
}
}
func TestInstallSHAutoDetectProxmoxKeepsRuntimeTypeUnpinned(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`if detect_proxmox; then`,
`log_info "Proxmox detected - enabling Proxmox integration"`,
`log_info " Proxmox type: auto-detect all installed services"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing unpinned Proxmox auto-detect contract: %s", needle)
}
}
forbidden := []string{
`detect_proxmox_type() {`,
`auto_type="$(detect_proxmox_type || true)"`,
`PROXMOX_TYPE="$auto_type"`,
}
for _, needle := range forbidden {
if strings.Contains(script, needle) {
t.Fatalf("install.sh preserved stale single-type Proxmox auto-detect contract: %s", needle)
}
}
}
func TestInstallSHExplainsCommandExecutionForProxmoxLXCDockerInventory(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`--enable-commands Enable Pulse command execution (disabled by default; required for Patrol actions and Proxmox LXC Docker inventory)`,
`log_info " Pulse command execution: $ENABLE_COMMANDS"`,
`log_info " Accepts Pulse-scoped command requests on this agent."`,
`log_info " On Proxmox nodes this is required for opted-in LXC Docker inventory via pct exec."`,
`log_info " The Pulse server must also be started with PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true."`,
`log_info " Command execution is off; enable only when Patrol actions or Proxmox LXC Docker inventory are needed."`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing command-execution disclosure: %s", needle)
}
}
if strings.Contains(script, "Enable AI command execution") {
t.Fatal("install.sh must not describe --enable-commands as AI command execution")
}
}
func TestInstallSHAcceptsLegacyBooleanFlagValues(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`--enable-host=true) ENABLE_HOST="true"; HOST_EXPLICIT="true"; shift ;;`,
`--enable-host=false) ENABLE_HOST="false"; HOST_EXPLICIT="true"; shift ;;`,
`--enable-docker=true) ENABLE_DOCKER="true"; DOCKER_EXPLICIT="true"; shift ;;`,
`--enable-docker=false) ENABLE_DOCKER="false"; DOCKER_EXPLICIT="true"; shift ;;`,
`--enable-kubernetes=true) ENABLE_KUBERNETES="true"; KUBERNETES_EXPLICIT="true"; shift ;;`,
`--enable-kubernetes=false) ENABLE_KUBERNETES="false"; KUBERNETES_EXPLICIT="true"; shift ;;`,
`--enable-proxmox=true) ENABLE_PROXMOX="true"; PROXMOX_EXPLICIT="true"; shift ;;`,
`--enable-proxmox=false) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing legacy boolean flag alias: %s", needle)
}
}
}
func TestInstallSHAgentDownloadIsServerVersionAware(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`"${PULSE_URL}/api/version"`,
`SERVER_VERSION="$(printf '%s' "$server_version_json" | sed -n 's/.*"version"`,
`DOWNLOAD_QUERY="${DOWNLOAD_QUERY}&serverVersion=${SERVER_VERSION}"`,
`log_info "Pulse server version: ${SERVER_VERSION}"`,
`NEW_VERSION_NORMALIZED="${NEW_VERSION#v}"`,
`SERVER_VERSION_NORMALIZED="${SERVER_VERSION#v}"`,
`"$NEW_VERSION_NORMALIZED" != "$SERVER_VERSION_NORMALIZED"`,
`Downloaded agent version (${NEW_VERSION}) does not match Pulse server version (${SERVER_VERSION})`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing version-aware agent download behavior: %s", needle)
}
}
}
// Regression test for #1527: the agent binary reports its version as "v6.0.4"
// while the server /api/version reports "6.0.4", so the mismatch check must
// strip a leading "v" before comparing and only warn on a genuine difference.
func TestInstallSHAgentVersionMismatchIgnoresVPrefix(t *testing.T) {
harness := `
set -euo pipefail
log_warn() { echo "WARN:$*"; }
SERVER_VERSION="$1"
NEW_VERSION="$2"
NEW_VERSION_NORMALIZED="${NEW_VERSION#v}"
SERVER_VERSION_NORMALIZED="${SERVER_VERSION#v}"
if [[ -n "$SERVER_VERSION" && -n "$NEW_VERSION" && "$NEW_VERSION" != "unknown" && "$NEW_VERSION_NORMALIZED" != "$SERVER_VERSION_NORMALIZED" ]]; then
log_warn "Downloaded agent version (${NEW_VERSION}) does not match Pulse server version (${SERVER_VERSION})."
fi
echo DONE
`
run := func(server, agent string) string {
out, err := exec.Command("bash", "-c", harness, "_", server, agent).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
return string(out)
}
if got := run("6.0.4", "v6.0.4"); strings.Contains(got, "WARN:") {
t.Fatalf("equal versions differing only by a leading v warned:\n%s", got)
}
if got := run("6.0.4", "6.0.4"); strings.Contains(got, "WARN:") {
t.Fatalf("identical versions warned:\n%s", got)
}
if got := run("6.0.4", "v6.0.3"); !strings.Contains(got, "WARN:") {
t.Fatalf("genuine version mismatch did not warn:\n%s", got)
}
}
func TestInstallSHAgentServiceSecurityDefaults(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`HEALTH_ADDR="${PULSE_HEALTH_ADDR:-}"`,
`if [[ -n "${PULSE_HEALTH_ADDR+x}" ]]; then`,
`--health-addr <addr> Health/metrics listener address (default: 127.0.0.1:9191; use "" to disable)`,
`if [[ "$HEALTH_ADDR_SET" == "true" ]]; then EXEC_ARG_ITEMS+=(--health-addr "$HEALTH_ADDR"); fi`,
`--health-addr) HEALTH_ADDR="$2"; HEALTH_ADDR_SET="true"; shift 2 ;;`,
`UMask=0077`,
`local no_new_privileges="true"`,
`NoNewPrivileges=${no_new_privileges}`,
`PrivateTmp=true`,
`ProtectKernelTunables=true`,
`ProtectKernelModules=true`,
`ProtectControlGroups=true`,
`LockPersonality=true`,
`local restrict_suidsgid="true"`,
`RestrictSUIDSGID=${restrict_suidsgid}`,
`SystemCallArchitectures=native`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing agent service security default: %s", needle)
}
}
}
func TestInstallSHAllowsProxmoxCommandAgentLXCAttach(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`systemd_agent_requires_lxc_attach() {`,
`if [[ "$ENABLE_COMMANDS" != "true" || "$ENABLE_PROXMOX" != "true" ]]; then`,
`""|pve|all)`,
`no_new_privileges="false"`,
`restrict_suidsgid="false"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing Proxmox command-agent LXC attach service handling: %s", needle)
}
}
}
func TestInstallSHPreflightChecksAgentDownloadArtifact(t *testing.T) {
var requestedDownloadPath string
var requestedDownloadArch string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/health":
w.WriteHeader(http.StatusOK)
case "/download/pulse-agent":
requestedDownloadPath = r.URL.Path
requestedDownloadArch = r.URL.Query().Get("arch")
w.Header().Set("X-Checksum-Sha256", strings.Repeat("a", 64))
w.WriteHeader(http.StatusOK)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
cmd := exec.Command(
"bash",
repoFile("scripts", "install.sh"),
"--url",
server.URL,
"--preflight-only",
"--output",
"json",
"--non-interactive",
)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("preflight failed: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, `"code":"agent_download_available"`) {
t.Fatalf("preflight did not report agent download availability:\n%s", got)
}
if requestedDownloadPath != "/download/pulse-agent" {
t.Fatalf("download path = %q, want /download/pulse-agent", requestedDownloadPath)
}
if requestedDownloadArch == "" {
t.Fatalf("download arch query was empty")
}
}
func TestInstallSHPreflightDoesNotRequireRoot(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`if [[ $EUID -ne 0 && "$PREFLIGHT_ONLY" != "true" ]]; then`,
`DOWNLOAD_CHECK_URL="${PULSE_URL}/download/${BINARY_NAME}?arch=${PF_ARCH_PARAM}"`,
`CURL_DOWNLOAD_CHECK_ARGS=(-fsSI --connect-timeout 5 --max-time 30 -D "$PREFLIGHT_HEADERS" -o /dev/null)`,
`grep -i '^X-Checksum-Sha256:' "$PREFLIGHT_HEADERS"`,
`"agent_download_available"`,
`"agent_download_unavailable"`,
`"agent_download_checksum_missing"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing non-root preflight download check: %s", needle)
}
}
}
func TestBuildPlistProgramArgumentsUsesSharedExecArgs(t *testing.T) {
script := `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "xml_escape") + `
` + extractInstallShellFunction(t, "append_plist_arg") + `
` + extractInstallShellFunction(t, "build_plist_program_arguments") + `
PULSE_URL="https://pulse.example/a&b"
PULSE_TOKEN="deadbeef"
INTERVAL="30s"
ENABLE_HOST="true"
ENABLE_DOCKER="false"
DOCKER_EXPLICIT="true"
ENABLE_KUBERNETES="true"
KUBECONFIG_PATH="/etc/kube config"
ENABLE_PROXMOX="true"
PROXMOX_TYPE="pbs"
INSECURE="true"
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
ENABLE_COMMANDS="true"
HEALTH_ADDR_SET="true"
HEALTH_ADDR=""
ENROLL="true"
KUBE_INCLUDE_ALL_PODS="true"
KUBE_INCLUDE_ALL_DEPLOYMENTS="true"
AGENT_ID="agent-1"
HOSTNAME_OVERRIDE="Richard's Mac & Mini"
STATE_DIR="/var/lib/pulse-agent"
DISK_EXCLUDES=("Time Machine")
build_plist_program_arguments "/usr/local/bin/pulse-agent"
printf '%s\n' "$PLIST_ARGS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
required := []string{
`<string>/usr/local/bin/pulse-agent</string>`,
`<string>https://pulse.example/a&amp;b</string>`,
`<string>--token-file</string>`,
`<string>/var/lib/pulse-agent/token</string>`,
`<string>--enable-docker=false</string>`,
`<string>--enable-proxmox</string>`,
`<string>--proxmox-type</string>`,
`<string>pbs</string>`,
`<string>--health-addr</string>`,
`<string>--hostname</string>`,
`<string>Richard's Mac &amp; Mini</string>`,
`<string>--state-dir</string>`,
`<string>/var/lib/pulse-agent</string>`,
`<string>--disk-exclude</string>`,
`<string>Time Machine</string>`,
}
for _, needle := range required {
if !strings.Contains(got, needle) {
t.Fatalf("plist args missing %s:\n%s", needle, got)
}
}
if strings.Contains(got, "--token</string>") || strings.Contains(got, "deadbeef") {
t.Fatalf("plist args leaked raw token:\n%s", got)
}
}
// TestConnectionEnvRecovery verifies the canonical helper logic that parses
// connection.env without using shell source (to prevent injection).
func TestConnectionEnvRecovery(t *testing.T) {
cases := []struct {
name string
content string
wantURL string
wantTok string
wantID string
wantHost string
wantInsecure string
wantCACert string
}{
{
name: "single-quoted values",
content: "PULSE_URL='http://192.168.0.98:7655'\nPULSE_TOKEN='abc123def'\nPULSE_AGENT_ID='agent-123'\nPULSE_HOSTNAME='node.local'\nPULSE_INSECURE_SKIP_VERIFY='true'\nPULSE_CACERT='/etc/pulse/ca.pem'\n",
wantURL: "http://192.168.0.98:7655",
wantTok: "abc123def",
wantID: "agent-123",
wantHost: "node.local",
wantInsecure: "true",
wantCACert: "/etc/pulse/ca.pem",
},
{
name: "unquoted values",
content: "PULSE_URL=http://10.0.0.1:7655\nPULSE_TOKEN=deadbeef\nPULSE_AGENT_ID=agent-456\nPULSE_HOSTNAME=node-two.local\nPULSE_INSECURE_SKIP_VERIFY=true\nPULSE_CACERT=/opt/pulse/ca.pem\n",
wantURL: "http://10.0.0.1:7655",
wantTok: "deadbeef",
wantID: "agent-456",
wantHost: "node-two.local",
wantInsecure: "true",
wantCACert: "/opt/pulse/ca.pem",
},
{
name: "https URL",
content: "PULSE_URL='https://pulse.example.com'\nPULSE_TOKEN='aabbccdd'\nPULSE_AGENT_ID='agent-https'\nPULSE_HOSTNAME='https-host'\nPULSE_INSECURE_SKIP_VERIFY='false'\nPULSE_CACERT='/usr/local/share/ca.pem'\n",
wantURL: "https://pulse.example.com",
wantTok: "aabbccdd",
wantID: "agent-https",
wantHost: "https-host",
wantInsecure: "false",
wantCACert: "/usr/local/share/ca.pem",
},
{
name: "extra whitespace lines",
content: "\nPULSE_URL='http://host:7655'\n\nPULSE_TOKEN='tok123'\n\nPULSE_AGENT_ID='agent-spaced'\nPULSE_HOSTNAME='spaced.local'\n\nPULSE_INSECURE_SKIP_VERIFY='true'\n\nPULSE_CACERT='/tmp/ca.pem'\n\n",
wantURL: "http://host:7655",
wantTok: "tok123",
wantID: "agent-spaced",
wantHost: "spaced.local",
wantInsecure: "true",
wantCACert: "/tmp/ca.pem",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
connFile := filepath.Join(dir, "connection.env")
if err := os.WriteFile(connFile, []byte(tc.content), 0600); err != nil {
t.Fatal(err)
}
// Run the same helper logic used by install.sh
script := `
CONN_ENV="` + connFile + `"
read_connection_state_value() {
local file="$1"
local key="$2"
awk -F= -v key="$key" '
$1 == key {
value = substr($0, index($0, "=") + 1)
sub(/^'\''/, "", value)
sub(/'\''$/, "", value)
print value
exit
}
' "$file" 2>/dev/null || true
}
PULSE_URL=$(read_connection_state_value "$CONN_ENV" "PULSE_URL")
PULSE_TOKEN=$(read_connection_state_value "$CONN_ENV" "PULSE_TOKEN")
PULSE_AGENT_ID=$(read_connection_state_value "$CONN_ENV" "PULSE_AGENT_ID")
PULSE_HOSTNAME=$(read_connection_state_value "$CONN_ENV" "PULSE_HOSTNAME")
PULSE_INSECURE_SKIP_VERIFY=$(read_connection_state_value "$CONN_ENV" "PULSE_INSECURE_SKIP_VERIFY")
PULSE_CACERT=$(read_connection_state_value "$CONN_ENV" "PULSE_CACERT")
echo "URL=${PULSE_URL}"
echo "TOKEN=${PULSE_TOKEN}"
echo "AGENT_ID=${PULSE_AGENT_ID}"
echo "HOSTNAME=${PULSE_HOSTNAME}"
echo "INSECURE=${PULSE_INSECURE_SKIP_VERIFY}"
echo "CACERT=${PULSE_CACERT}"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
gotURL, gotTok, gotID, gotHost, gotInsecure, gotCACert := "", "", "", "", "", ""
for _, line := range lines {
if strings.HasPrefix(line, "URL=") {
gotURL = strings.TrimPrefix(line, "URL=")
}
if strings.HasPrefix(line, "TOKEN=") {
gotTok = strings.TrimPrefix(line, "TOKEN=")
}
if strings.HasPrefix(line, "AGENT_ID=") {
gotID = strings.TrimPrefix(line, "AGENT_ID=")
}
if strings.HasPrefix(line, "HOSTNAME=") {
gotHost = strings.TrimPrefix(line, "HOSTNAME=")
}
if strings.HasPrefix(line, "INSECURE=") {
gotInsecure = strings.TrimPrefix(line, "INSECURE=")
}
if strings.HasPrefix(line, "CACERT=") {
gotCACert = strings.TrimPrefix(line, "CACERT=")
}
}
if gotURL != tc.wantURL {
t.Errorf("URL = %q, want %q", gotURL, tc.wantURL)
}
if gotTok != tc.wantTok {
t.Errorf("TOKEN = %q, want %q", gotTok, tc.wantTok)
}
if gotID != tc.wantID {
t.Errorf("AGENT_ID = %q, want %q", gotID, tc.wantID)
}
if gotHost != tc.wantHost {
t.Errorf("HOSTNAME = %q, want %q", gotHost, tc.wantHost)
}
if gotInsecure != tc.wantInsecure {
t.Errorf("INSECURE = %q, want %q", gotInsecure, tc.wantInsecure)
}
if gotCACert != tc.wantCACert {
t.Errorf("CACERT = %q, want %q", gotCACert, tc.wantCACert)
}
})
}
}
// TestAgentIDFileRecovery verifies the agent-id file lookup priority:
// /var/lib/pulse-agent/agent-id > /boot/config/plugins/pulse-agent/agent-id
func TestAgentIDFileRecovery(t *testing.T) {
cases := []struct {
name string
files map[string]string // relative path -> content
wantID string
}{
{
name: "primary location",
files: map[string]string{
"var/lib/pulse-agent/agent-id": "uuid-primary",
},
wantID: "uuid-primary",
},
{
name: "secondary location (Unraid)",
files: map[string]string{
"boot/config/plugins/pulse-agent/agent-id": "uuid-unraid",
},
wantID: "uuid-unraid",
},
{
name: "primary takes precedence",
files: map[string]string{
"var/lib/pulse-agent/agent-id": "uuid-primary",
"boot/config/plugins/pulse-agent/agent-id": "uuid-unraid",
},
wantID: "uuid-primary",
},
{
name: "no file found",
files: map[string]string{},
wantID: "",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
for relPath, content := range tc.files {
fullPath := filepath.Join(root, relPath)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// Replicate the install.sh agent-id recovery loop
script := `
AGENT_ID=""
for aid_path in "` + root + `/var/lib/pulse-agent/agent-id" "` + root + `/boot/config/plugins/pulse-agent/agent-id"; do
if [[ -f "$aid_path" ]]; then
AGENT_ID=$(cat "$aid_path")
break
fi
done
echo "$AGENT_ID"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
if got != tc.wantID {
t.Errorf("agent-id = %q, want %q", got, tc.wantID)
}
})
}
}
func TestInstallSHUsesHostnameOverrideForUninstallLookup(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`LOOKUP_HOSTNAME="$HOSTNAME_OVERRIDE"`,
`if [[ -z "$LOOKUP_HOSTNAME" ]]; then`,
`LOOKUP_HOSTNAME=$(hostname 2>/dev/null || true)`,
`LOOKUP_HOSTNAME_ESCAPED=$(url_encode "$LOOKUP_HOSTNAME")`,
`"${PULSE_URL}/api/agents/agent/lookup?hostname=${LOOKUP_HOSTNAME_ESCAPED}"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing uninstall hostname override lookup handling: %s", needle)
}
}
}
func TestInstallSHUrlEncodesHostnameLookupQuery(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`url_encode() {`,
`printf -v encoded '%%%02X' "'$c"`,
`LOOKUP_HOSTNAME_ESCAPED=$(url_encode "$LOOKUP_HOSTNAME")`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing encoded hostname lookup transport: %s", needle)
}
}
}
func TestInstallSHPersistsIdentityInConnectionEnv(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`write_connection_state_value() {`,
`read_connection_state_value() {`,
`recover_connection_state() {`,
`find_connection_state_file() {`,
`write_connection_state_value "$conn_tmp" "PULSE_STATE_DIR" "$state_dir"`,
`write_connection_state_value "$conn_tmp" "PULSE_TOKEN_FILE" "$RUNTIME_TOKEN_FILE"`,
`write_connection_state_value "$conn_tmp" "PULSE_AGENT_ID" "$AGENT_ID"`,
`write_connection_state_value "$conn_tmp" "PULSE_HOSTNAME" "$HOSTNAME_OVERRIDE"`,
`write_connection_state_value "$conn_tmp" "PULSE_INSECURE_SKIP_VERIFY" "true"`,
`write_connection_state_value "$conn_tmp" "PULSE_CACERT" "$CURL_CA_BUNDLE"`,
`recover_connection_state "$lifecycle_conn_env"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing persisted identity recovery: %s", needle)
}
}
}
func TestInstallSHRecoversSavedStateForPartialUninstallContext(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
needles := []string{
`if [[ "$UPDATE_ONLY" == "true" || "$UNINSTALL" == "true" ]]; then`,
`# An explicit state directory is authoritative.`,
`lifecycle_conn_env=$(find_connection_state_file || true)`,
`recover_connection_state "$lifecycle_conn_env"`,
}
for _, needle := range needles {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing partial uninstall saved-state recovery guard: %s", needle)
}
}
}
func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`--update Update an existing agent using saved connection state`,
`UPDATE_ONLY="false"`,
`--update) UPDATE_ONLY="true"; shift ;;`,
`if [[ "$UPDATE_ONLY" == "true" || "$UNINSTALL" == "true" ]]; then`,
`lifecycle_conn_env=$(find_connection_state_file || true)`,
`recover_connection_state "$lifecycle_conn_env"`,
`recover_connection_state_from_existing_agent() {`,
`recover_connection_state_from_running_agent`,
`recover_connection_state_from_systemd_unit`,
`recover_connection_state_from_launchd_plist`,
`recover_connection_state_from_service_scripts`,
`running_agent_arg_stream() {`,
`running_agent_env_stream() {`,
`recover_connection_state_from_arg_stream`,
`recover_token_from_default_agent_token_file() {`,
`normalize_recovered_agent_arg_key() {`,
`-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-report-ip|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`,
`--enable-host|-enable-host|--enable-host=true|-enable-host=true)`,
`recover_connection_state_from_env_stream`,
`recovered_connection_state_ready() {`,
`update_connection_state_incomplete() {`,
`[[ "$RECOVERED_AGENT_ARG_STATE" == "true" ]] && recovered_connection_state_ready`,
`[[ "$RECOVERED_AGENT_ENV_STATE" == "true" ]] && recovered_connection_state_ready`,
`if update_connection_state_incomplete; then`,
`recover_connection_state_from_existing_agent || true`,
`if [[ "$UPDATE_ONLY" == "true" && ( -z "$PULSE_URL" || -z "$PULSE_TOKEN" ) ]]; then`,
`recover_agent_id_from_state_file() {`,
`AGENT_ID=$(recover_agent_id_from_state_file || true)`,
`No existing Pulse Agent connection state found. Use the install command instead.`,
`if [[ "$UPDATE_ONLY" == "true" && "$UPGRADE_MODE" != "true" ]]; then`,
`No existing Pulse Agent installation found to update. Use the install command instead.`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing saved-state update mode contract: %s", needle)
}
}
}
func TestInstallSHRecoversV5ProcessArgsForSavedStateUpdate(t *testing.T) {
script := `
fail() { echo "FAIL:$1"; exit 99; }
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
--url
http://192.168.2.96:7655
--token
deadbeef
--interval
30s
--enable-host
--enable-docker
--insecure
--agent-id
agent-123
--hostname
pve-one
ARGS
build_exec_args
printf 'URL=%s\nTOKEN=%s\nDOCKER=%s\nDOCKER_EXPLICIT=%s\nINSECURE=%s\nAGENT_ID=%s\nHOSTNAME=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_DOCKER" "$DOCKER_EXPLICIT" "$INSECURE" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$EXEC_ARGS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
required := []string{
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"DOCKER=true",
"DOCKER_EXPLICIT=true",
"INSECURE=true",
"AGENT_ID=agent-123",
"HOSTNAME=pve-one",
"--token-file /var/lib/pulse-agent/token",
"--enable-docker",
}
for _, needle := range required {
if !strings.Contains(got, needle) {
t.Fatalf("recovered update state missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("recovered service args leaked raw token:\n%s", got)
}
}
func TestInstallSHRecoversV5ProcessArgsWithoutProcfs(t *testing.T) {
script := `
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
TRUENAS_STATE_DIR="/data/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
BINARY_NAME="pulse-agent"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "split_recovered_shell_words") + `
` + extractInstallShellFunction(t, "running_agent_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_running_agent") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
collect_running_agent_pids() { printf '%s\n' 4242; }
running_agent_env_stream() { return 1; }
ps() {
printf '%s\n' "/usr/local/bin/pulse-agent -url http://192.168.2.96:7655 -token deadbeef -enable-host -enable-docker -insecure -agent-id firewall-1 -hostname 'edge firewall'"
}
if recover_connection_state_from_running_agent; then
echo "READY"
else
echo "NOT_READY"
fi
build_exec_args
printf 'URL=%s\nTOKEN=%s\nDOCKER=%s\nINSECURE=%s\nAGENT_ID=%s\nHOSTNAME=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_DOCKER" "$INSECURE" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$EXEC_ARGS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"DOCKER=true",
"INSECURE=true",
"AGENT_ID=firewall-1",
"HOSTNAME=edge firewall",
"--token-file /var/lib/pulse-agent/token",
} {
if !strings.Contains(got, needle) {
t.Fatalf("non-procfs process recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("non-procfs process recovery leaked raw token into service args:\n%s", got)
}
}
func TestInstallSHRecoversV5FreeBSDRCServiceState(t *testing.T) {
rcScript := filepath.Join(t.TempDir(), "pulse-agent")
if err := os.WriteFile(rcScript, []byte(`#!/bin/sh
command="/usr/local/bin/pulse-agent"
command_args="-url http://192.168.2.96:7655 -token deadbeef -enable-host -insecure -agent-id firewall-1 -hostname 'edge firewall'"
export PULSE_CACERT='/conf/pulse-ca.pem'
`), 0600); err != nil {
t.Fatalf("write rc.d fixture: %v", err)
}
script := `
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
TRUENAS_STATE_DIR="/data/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
AGENT_NAME="pulse-agent"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_env_stream") + `
` + extractInstallShellFunction(t, "split_recovered_shell_words") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_service_scripts") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
if recover_connection_state_from_service_scripts "${PULSE_TEST_RC_SCRIPT:?}"; then
echo "READY"
else
echo "NOT_READY"
fi
build_exec_args
printf 'URL=%s\nTOKEN=%s\nINSECURE=%s\nAGENT_ID=%s\nHOSTNAME=%s\nCACERT=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$INSECURE" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$CURL_CA_BUNDLE" "$EXEC_ARGS"
`
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "PULSE_TEST_RC_SCRIPT="+rcScript)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"INSECURE=true",
"AGENT_ID=firewall-1",
"HOSTNAME=edge firewall",
"CACERT=/conf/pulse-ca.pem",
"--token-file /var/lib/pulse-agent/token",
} {
if !strings.Contains(got, needle) {
t.Fatalf("FreeBSD rc.d recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("FreeBSD rc.d recovery leaked raw token into service args:\n%s", got)
}
}
func TestInstallSHRejectsPartialRecoveredProcessConnectionState(t *testing.T) {
script := `
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
if recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
--url
http://192.168.2.96:7655
--enable-host
--agent-id
agent-123
ARGS
then
echo "UNEXPECTED_SUCCESS"
else
echo "EXPECTED_FAILURE"
fi
printf 'URL=%s\nTOKEN=%s\nAGENT_ID=%s\n' "$PULSE_URL" "$PULSE_TOKEN" "$AGENT_ID"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, "EXPECTED_FAILURE") {
t.Fatalf("partial recovered state should fail closed:\n%s", got)
}
if strings.Contains(got, "UNEXPECTED_SUCCESS") {
t.Fatalf("partial recovered state reported success:\n%s", got)
}
if !strings.Contains(got, "URL=http://192.168.2.96:7655") || !strings.Contains(got, "AGENT_ID=agent-123") {
t.Fatalf("partial recovery should still preserve non-secret fields for later sources:\n%s", got)
}
if !strings.Contains(got, "TOKEN=") {
t.Fatalf("expected token to remain empty:\n%s", got)
}
}
func TestInstallSHRecoversLegacyDefaultTokenFileForSavedStateUpdate(t *testing.T) {
stateDir := t.TempDir()
if err := os.WriteFile(filepath.Join(stateDir, "token"), []byte("deadbeef\n"), 0600); err != nil {
t.Fatalf("write legacy token file: %v", err)
}
script := `
fail() { echo "FAIL:$1"; exit 99; }
PULSE_URL="http://192.168.2.96:7655"
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="true"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="${PULSE_TEST_STATE_DIR:?}"
TRUENAS_STATE_DIR="${PULSE_TEST_STATE_DIR:?}/truenas"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="${STATE_DIR}/token"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
if recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
-url
http://192.168.2.96:7655
-interval
30s
-enable-host
-enable-docker
-agent-id
machine-1
-hostname
docker1
ARGS
then
echo "READY"
else
echo "NOT_READY"
fi
build_exec_args
printf 'URL=%s\nTOKEN=%s\nDOCKER=%s\nAGENT_ID=%s\nHOSTNAME=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_DOCKER" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$EXEC_ARGS"
`
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "PULSE_TEST_STATE_DIR="+stateDir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"DOCKER=true",
"AGENT_ID=machine-1",
"HOSTNAME=docker1",
"--token-file",
stateDir + "/token",
} {
if !strings.Contains(got, needle) {
t.Fatalf("legacy default token recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("legacy default token recovery leaked raw token into service args:\n%s", got)
}
}
func TestInstallSHCombinesRecoveredProcessArgsAndEnvConnectionState(t *testing.T) {
script := `
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_env_stream") + `
if recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
--url
http://192.168.2.96:7655
ARGS
then
echo "ARGS_READY"
else
echo "ARGS_PARTIAL"
fi
if recover_connection_state_from_env_stream <<'ENV'
PULSE_TOKEN=deadbeef
ENV
then
echo "ENV_READY"
else
echo "ENV_PARTIAL"
fi
printf 'URL=%s\nTOKEN=%s\n' "$PULSE_URL" "$PULSE_TOKEN"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"ARGS_PARTIAL",
"ENV_READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
} {
if !strings.Contains(got, needle) {
t.Fatalf("combined recovery missing %q:\n%s", needle, got)
}
}
}
func TestInstallSHUpdateModeMergesExplicitURLWithRunningV5ProcessState(t *testing.T) {
script := `
fail() { echo "FAIL:$1"; exit 99; }
log_info() { :; }
UPDATE_ONLY="true"
PULSE_URL="http://192.168.2.96:7655"
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="true"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "update_connection_state_incomplete") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
find_connection_state_file() { return 1; }
recover_agent_id_from_state_file() { return 1; }
recover_connection_state_from_existing_agent() {
recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
-url
http://192.168.2.96:7655
-token
deadbeef
-interval
30s
-enable-host
-enable-docker
-agent-id
machine-1
-hostname
docker1
ARGS
}
if [[ "$UPDATE_ONLY" == "true" ]]; then
if update_connection_state_incomplete; then
update_conn_env=$(find_connection_state_file || true)
if [[ -n "$update_conn_env" ]]; then
recover_connection_state "$update_conn_env"
fi
if update_connection_state_incomplete; then
recover_connection_state_from_existing_agent || true
fi
if [[ -n "$PULSE_URL" && -n "$PULSE_TOKEN" ]]; then
echo "READY"
elif [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" ]]; then
fail "No existing Pulse Agent connection state found. Use the install command instead."
fi
if [[ -z "$AGENT_ID" ]]; then
AGENT_ID=$(recover_agent_id_from_state_file || true)
fi
fi
fi
build_exec_args
printf 'URL=%s\nTOKEN=%s\nDOCKER=%s\nDOCKER_EXPLICIT=%s\nAGENT_ID=%s\nHOSTNAME=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_DOCKER" "$DOCKER_EXPLICIT" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$EXEC_ARGS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
required := []string{
"READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"DOCKER=true",
"DOCKER_EXPLICIT=true",
"AGENT_ID=machine-1",
"HOSTNAME=docker1",
"--token-file /var/lib/pulse-agent/token",
}
for _, needle := range required {
if !strings.Contains(got, needle) {
t.Fatalf("reported v5 update recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("reported v5 update recovery leaked raw token into service args:\n%s", got)
}
}
func TestInstallSHUsesCanonicalServiceLifecycleHelpers(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`stop_existing_agent_service() {`,
`restart_systemd_agent_service() {`,
`restart_openrc_agent_service() {`,
`restart_service_command_agent() {`,
`restart_sysv_agent_service() {`,
`stop_existing_agent_service || true`,
`restart_systemd_agent_service`,
`restart_openrc_agent_service`,
`restart_service_command_agent`,
`restart_sysv_agent_service "$RCSCRIPT"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical service lifecycle helper usage: %s", needle)
}
}
}
func TestInstallSHUsesCanonicalServiceTeardownHelpers(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`teardown_systemd_agent_service() {`,
`teardown_openrc_agent_service() {`,
`teardown_freebsd_agent_service() {`,
`teardown_sysv_agent_service() {`,
`teardown_systemd_agent_service`,
`teardown_freebsd_agent_service "/usr/local/etc/rc.d/${AGENT_NAME}"`,
`teardown_openrc_agent_service`,
`teardown_sysv_agent_service "/etc/init.d/${AGENT_NAME}"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical service teardown helper usage: %s", needle)
}
}
}
func TestWriteTrueNASBootstrapScriptUsesCanonicalRenderer(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`write_truenas_bootstrap_script() {`,
`require_bootstrap_file() {`,
`sync_runtime_binary() {`,
`link_service_artifact() {`,
`start_agent_service() {`,
`ensure_freebsd_agent_enabled() {`,
`service_link="/etc/systemd/system/${AGENT_NAME}.service"`,
`service_link="/usr/local/etc/rc.d/${AGENT_NAME}"`,
`write_truenas_bootstrap_script "$(uname -s)"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical TrueNAS bootstrap renderer content: %s", needle)
}
}
if strings.Count(script, `cat > "$TRUENAS_BOOTSTRAP_SCRIPT"`) != 1 {
t.Fatalf("expected one canonical TrueNAS bootstrap writer, found %d", strings.Count(script, `cat > "$TRUENAS_BOOTSTRAP_SCRIPT"`))
}
}
func TestInstallSHUsesCanonicalQNAPBootstrapRenderer(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`detect_qnap_data_volume() {`,
`find_qnap_state_dir() {`,
`remove_qnap_autorun_block() {`,
`write_qnap_wrapper_script() {`,
`append_qnap_autorun_block() {`,
`select_platform_state_dir "${QNAP_VOL}/.pulse-agent"`,
`write_qnap_wrapper_script "$WRAPPER_SCRIPT" "$RUNTIME_BINARY" "$QNAP_STORED_BINARY"`,
`append_qnap_autorun_block "$AUTORUN_PATH" "$WRAPPER_SCRIPT" "$STATE_DIR"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f /var/log/${AGENT_NAME}.log"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical QNAP bootstrap ownership: %s", needle)
}
}
}
func TestInstallSHUsesQNAPStateForUninstallRecovery(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`qnap_state_dir=$(find_qnap_state_dir || true)`,
`aid_paths+=("$qnap_state_dir/agent-id")`,
`if [[ -n "$qnap_state_dir" ]] && [[ -f "$qnap_state_dir/connection.env" ]]; then`,
`remove_qnap_autorun_block "$AUTORUN_PATH"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing QNAP uninstall continuity handling: %s", needle)
}
}
}
func TestInstallSHUsesSharedServiceRenderers(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`render_systemd_agent_unit() {`,
`render_freebsd_rc_agent_script() {`,
`render_systemd_agent_unit "$UNIT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}" "network.target" "" "" ""`,
`render_systemd_agent_unit "$TRUENAS_SERVICE_STORAGE" "${TRUENAS_RUNTIME_BINARY}" "${EXEC_ARGS}" "network-online.target docker.service" "network-online.target" "root" "${TRUENAS_LOG_TARGET}"`,
`render_systemd_agent_unit "$UNIT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}" "network-online.target docker.service" "network-online.target" "root" ""`,
`render_freebsd_rc_agent_script "$TRUENAS_SERVICE_STORAGE" "${TRUENAS_RUNTIME_BINARY}" "${EXEC_ARGS}"`,
`render_freebsd_rc_agent_script "$RCSCRIPT" "${INSTALL_DIR}/${BINARY_NAME}" "${EXEC_ARGS}"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing shared service renderer usage: %s", needle)
}
}
}
func TestInstallSHPersistsRootlessContainerRuntimeServiceEnvironment(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`ROOTLESS_RUNTIME_SOCKET_URI=""`,
`discover_rootless_container_runtime() {`,
`discover_single_socket_match "/run/user/*/docker.sock"`,
`discover_single_socket_match "/run/user/*/podman/podman.sock"`,
`append_service_env "DOCKER_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"`,
`append_service_env "PULSE_DOCKER_RUNTIME" "podman"`,
`append_service_env "CONTAINER_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"`,
`append_service_env "PODMAN_HOST" "$ROOTLESS_RUNTIME_SOCKET_URI"`,
`append_service_env "XDG_RUNTIME_DIR" "$ROOTLESS_RUNTIME_XDG_DIR"`,
`env_line="$SYSTEMD_ENV_LINES"`,
`local service_env_lines="$SHELL_EXPORT_LINES"`,
`</array>${PLIST_ENV_BLOCK}`,
`respawn limit 5 10${UPSTART_ENV_LINES}`,
`sed -i "s|SSL_CERT_FILE_PLACEHOLDER|${SED_EXPORT_LINES}|g" "$INITSCRIPT"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing rootless service environment persistence contract: %s", needle)
}
}
}
func TestInstallSHServiceEnvAccumulatorRendersRootlessSocketVariables(t *testing.T) {
script := `
APPLIED_SERVICE_ENV_KEYS="|"
SYSTEMD_ENV_LINES=""
SHELL_EXPORT_LINES=""
UPSTART_ENV_LINES=""
SED_EXPORT_LINES=""
PLIST_ENV_ENTRIES=""
PLIST_ENV_BLOCK=""
` + extractInstallShellFunction(t, "xml_escape") + `
` + extractInstallShellFunction(t, "service_env_has_key") + `
` + extractInstallShellFunction(t, "shell_export_value") + `
` + extractInstallShellFunction(t, "append_service_env") + `
` + extractInstallShellFunction(t, "finalize_plist_env_block") + `
append_service_env "DOCKER_HOST" "unix:///run/user/1000/docker.sock"
append_service_env "XDG_RUNTIME_DIR" "/run/user/1000"
append_service_env "DOCKER_HOST" "unix:///run/user/2000/docker.sock"
finalize_plist_env_block
printf '%s\n---shell---\n%s\n---sed---\n%s\n---plist---\n%s\n' "$SYSTEMD_ENV_LINES" "$SHELL_EXPORT_LINES" "$SED_EXPORT_LINES" "$PLIST_ENV_BLOCK"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
required := []string{
`Environment="DOCKER_HOST=unix:///run/user/1000/docker.sock"`,
`Environment="XDG_RUNTIME_DIR=/run/user/1000"`,
`export DOCKER_HOST="unix:///run/user/1000/docker.sock"`,
`export XDG_RUNTIME_DIR="/run/user/1000"`,
`export DOCKER_HOST="unix:///run/user/1000/docker.sock"; export XDG_RUNTIME_DIR="/run/user/1000"`,
`<key>DOCKER_HOST</key>`,
`<string>unix:///run/user/1000/docker.sock</string>`,
}
for _, needle := range required {
if !strings.Contains(got, needle) {
t.Fatalf("service env output missing %s:\n%s", needle, got)
}
}
if strings.Contains(got, "unix:///run/user/2000/docker.sock") {
t.Fatalf("service env accumulator did not ignore duplicate key:\n%s", got)
}
}
func TestInstallSHDiscoverSingleSocketMatch(t *testing.T) {
tmpDir, err := os.MkdirTemp("/tmp", "pulse-sock-*")
if err != nil {
t.Fatalf("mktemp socket dir: %v", err)
}
defer os.RemoveAll(tmpDir)
socketPath := filepath.Join(tmpDir, "docker.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatalf("listen unix socket: %v", err)
}
defer listener.Close()
script := `
` + extractInstallShellFunction(t, "discover_single_socket_match") + `
discover_single_socket_match "` + filepath.ToSlash(tmpDir) + `/*.sock"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if strings.TrimSpace(string(out)) != filepath.ToSlash(socketPath) {
t.Fatalf("socket match = %q, want %q", strings.TrimSpace(string(out)), filepath.ToSlash(socketPath))
}
}
func TestInstallSHFreeBSDRendererUsesDaemonSupervisorPidfile(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`child_pidfile="/var/run/\${name}.child.pid"`,
`pulse_agent_supervisor_pid()`,
`parent_pid=\$(ps -o ppid= -p "\${agent_pid}" 2>/dev/null | tr -d '[:space:]')`,
`daemon:*)`,
`/usr/sbin/daemon -r -P \${pidfile} -p \${child_pidfile} -f "\${command}" \${command_args}`,
`kill -KILL "\${supervisor_pid}" 2>/dev/null || true`,
`rm -f \${pidfile} \${child_pidfile}`,
`legacy child pid \${legacy_pid} supervised by pid \${legacy_supervisor}`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing FreeBSD daemon supervisor contract: %s", needle)
}
}
if strings.Contains(script, `/usr/sbin/daemon -r -p \${pidfile}`) {
t.Fatal("install.sh still writes the child pid to the service pidfile under daemon -r")
}
}
func TestInstallSHUsesCanonicalCompletionHelper(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`complete_installation_flow() {`,
`save_connection_info "$state_dir"`,
`json_event "complete" "updated" "Installation updated"`,
`json_event "complete" "installed" "Installation installed"`,
`json_event "complete" "updated_unhealthy" "Agent updated but not responding"`,
`json_event "complete" "installed_unhealthy" "Agent installed but not responding"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f $LOG_FILE"`,
`complete_installation_flow "$UNRAID_STORAGE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f /var/log/${AGENT_NAME}.log"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f /var/log/${AGENT_NAME}.log"`,
`complete_installation_flow "$TRUENAS_STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." ""`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/messages"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "journalctl -u ${AGENT_NAME} --no-pager -n 20"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent restarted with new configuration." "tail -f /var/log/${AGENT_NAME}.log"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical completion helper usage: %s", needle)
}
}
}
func TestInstallSHUsesCanonicalFreeBSDAgentEnablement(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`freebsd_enable_snippet() {`,
`ensure_freebsd_agent_enabled() {`,
`service_management_functions="$(freebsd_enable_snippet)`,
`ensure_freebsd_agent_enabled`,
`apply_freebsd_agent_enablement() {`,
`eval "$(freebsd_enable_snippet)"`,
`apply_freebsd_agent_enablement`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical FreeBSD enablement ownership: %s", needle)
}
}
if strings.Count(script, `grep -q "pulse_agent_enable" /etc/rc.conf`) != 1 {
t.Fatalf("expected one canonical FreeBSD enablement definition, found %d", strings.Count(script, `grep -q "pulse_agent_enable" /etc/rc.conf`))
}
}
func TestInstallSHUsesCanonicalFreeBSDAgentTeardown(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`teardown_freebsd_agent_service() {`,
`"$service_path" stop 2>/dev/null || true`,
`sysrc -x pulse_agent_enable >/dev/null 2>&1 || true`,
`rm -f /usr/local/etc/rc.d/pulse_agent.sh`,
`rm -f /var/run/pulse_agent.pid /var/run/pulse_agent.child.pid`,
`log_info "Removing FreeBSD rc.d installation..."`,
`teardown_freebsd_agent_service "/usr/local/etc/rc.d/${AGENT_NAME}"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical FreeBSD teardown contract: %s", needle)
}
}
}
func TestInstallSHUsesCanonicalSysVEnablementHelper(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`enable_sysv_agent_service() {`,
`update-rc.d "${AGENT_NAME}" defaults >/dev/null 2>&1 || true`,
`chkconfig --add "${AGENT_NAME}" >/dev/null 2>&1 || true`,
`chkconfig "${AGENT_NAME}" on >/dev/null 2>&1 || true`,
`enable_sysv_agent_service "$INITSCRIPT"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing canonical SysV enablement ownership: %s", needle)
}
}
if strings.Count(script, `update-rc.d "${AGENT_NAME}" defaults >/dev/null 2>&1 || true`) != 1 {
t.Fatalf("expected one canonical SysV update-rc.d enable path, found %d", strings.Count(script, `update-rc.d "${AGENT_NAME}" defaults >/dev/null 2>&1 || true`))
}
}
// TestUnraidGoScriptCleanup verifies the sed commands that remove pulse entries
// from /boot/config/go without disturbing other entries.
func TestUnraidGoScriptCleanup(t *testing.T) {
cases := []struct {
name string
before string
after string
}{
{
name: "pulse entry with trailing blank line",
before: `#!/bin/bash
/boot/config/pulse/telegraf/start_telegraf.sh
# Pulse Agent
bash /boot/config/plugins/pulse-agent/start-pulse-agent.sh
# Other stuff
echo hello
`,
// The comment and command lines are removed; the trailing blank line remains.
// This is harmless in /boot/config/go.
after: `#!/bin/bash
/boot/config/pulse/telegraf/start_telegraf.sh
# Other stuff
echo hello
`,
},
{
name: "pulse entry without trailing blank line",
before: `#!/bin/bash
# Pulse Agent
bash /boot/config/plugins/pulse-agent/start-pulse-agent.sh
# Other stuff
echo hello
`,
after: `#!/bin/bash
# Other stuff
echo hello
`,
},
{
name: "no pulse entries - unchanged",
before: `#!/bin/bash
echo hello
echo world
`,
after: `#!/bin/bash
echo hello
echo world
`,
},
{
name: "telegraf line containing pulse is kept",
before: `#!/bin/bash
/boot/config/pulse/telegraf/start_telegraf.sh
# Pulse Agent
bash /boot/config/plugins/pulse-agent/start-pulse-agent.sh
echo done
`,
// The telegraf line is NOT removed (no "pulse-agent" in it).
// Comment and command lines are deleted individually.
after: `#!/bin/bash
/boot/config/pulse/telegraf/start_telegraf.sh
echo done
`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
goScript := filepath.Join(dir, "go")
if err := os.WriteFile(goScript, []byte(tc.before), 0755); err != nil {
t.Fatal(err)
}
// Run the exact same sed commands from install.sh (line-by-line, not range-based)
script := `
GO_SCRIPT="` + goScript + `"
# Remove unified agent entries
sed -i '' '/^# Pulse Agent$/d' "$GO_SCRIPT" 2>/dev/null || sed -i '/^# Pulse Agent$/d' "$GO_SCRIPT" 2>/dev/null || true
sed -i '' '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || sed -i '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || true
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got, err := os.ReadFile(goScript)
if err != nil {
t.Fatal(err)
}
if string(got) != tc.after {
t.Errorf("go script mismatch:\n--- got ---\n%s\n--- want ---\n%s", got, tc.after)
}
})
}
}
// TestAPIDeregistrationCurl verifies the curl command sends the correct
// JSON payload and headers to the canonical uninstall endpoint.
func TestAPIDeregistrationCurl(t *testing.T) {
var (
mu sync.Mutex
gotMethod string
gotPath string
gotBody map[string]string
gotHeaders http.Header
)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
gotMethod = r.Method
gotPath = r.URL.Path
gotHeaders = r.Header.Clone()
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &gotBody)
w.WriteHeader(200)
w.Write([]byte(`{"success":true}`))
}))
defer srv.Close()
agentID := "test-uuid-1234"
token := "deadbeef0123456789"
script := `
PULSE_URL="` + srv.URL + `"
PULSE_TOKEN="` + token + `"
AGENT_ID="` + agentID + `"
CURL_ARGS=(-fsSL --connect-timeout 5 -X POST -H "Content-Type: application/json")
if [[ -n "$PULSE_TOKEN" ]]; then CURL_ARGS+=(-H "X-API-Token: ${PULSE_TOKEN}"); fi
curl "${CURL_ARGS[@]}" -d "{\"agentId\": \"${AGENT_ID}\"}" "${PULSE_URL}/api/agents/agent/uninstall" >/dev/null 2>&1 || true
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
mu.Lock()
defer mu.Unlock()
if gotMethod != "POST" {
t.Errorf("method = %q, want POST", gotMethod)
}
if gotPath != "/api/agents/agent/uninstall" {
t.Errorf("path = %q, want /api/agents/agent/uninstall", gotPath)
}
if gotBody["agentId"] != agentID {
t.Errorf("body agentId = %q, want %q", gotBody["agentId"], agentID)
}
if got := gotHeaders.Get("X-API-Token"); got != token {
t.Errorf("X-API-Token = %q, want %q", got, token)
}
if got := gotHeaders.Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type = %q, want application/json", got)
}
}
func TestAPIDeregistrationCurlWithoutToken(t *testing.T) {
var (
mu sync.Mutex
gotMethod string
gotPath string
gotBody map[string]string
gotHeaders http.Header
)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
gotMethod = r.Method
gotPath = r.URL.Path
gotHeaders = r.Header.Clone()
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &gotBody)
w.WriteHeader(200)
w.Write([]byte(`{"success":true}`))
}))
defer srv.Close()
agentID := "test-uuid-5678"
script := `
PULSE_URL="` + srv.URL + `"
PULSE_TOKEN=""
AGENT_ID="` + agentID + `"
CURL_ARGS=(-fsSL --connect-timeout 5 -X POST -H "Content-Type: application/json")
if [[ -n "$PULSE_TOKEN" ]]; then CURL_ARGS+=(-H "X-API-Token: ${PULSE_TOKEN}"); fi
curl "${CURL_ARGS[@]}" -d "{\"agentId\": \"${AGENT_ID}\"}" "${PULSE_URL}/api/agents/agent/uninstall" >/dev/null 2>&1 || true
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
mu.Lock()
defer mu.Unlock()
if gotMethod != "POST" {
t.Errorf("method = %q, want POST", gotMethod)
}
if gotPath != "/api/agents/agent/uninstall" {
t.Errorf("path = %q, want /api/agents/agent/uninstall", gotPath)
}
if gotBody["agentId"] != agentID {
t.Errorf("body agentId = %q, want %q", gotBody["agentId"], agentID)
}
if got := gotHeaders.Get("X-API-Token"); got != "" {
t.Errorf("X-API-Token = %q, want empty", got)
}
if got := gotHeaders.Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type = %q, want application/json", got)
}
}
func extractInstallShellFunction(t *testing.T, name string) string {
t.Helper()
content, err := os.ReadFile(filepath.Join("..", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
pattern := regexp.MustCompile(`(?ms)^` + regexp.QuoteMeta(name) + `\(\) \{\n.*?^\}`)
match := pattern.Find(content)
if match == nil {
t.Fatalf("could not find %s in install.sh", name)
}
return string(match)
}
func extractRootInstallShellFunction(t *testing.T, name string) string {
t.Helper()
content, err := os.ReadFile(filepath.Join("..", "..", "install.sh"))
if err != nil {
t.Fatalf("read root install.sh: %v", err)
}
pattern := regexp.MustCompile(`(?ms)^` + regexp.QuoteMeta(name) + `\(\) \{\n.*?^\}`)
match := pattern.Find(content)
if match == nil {
t.Fatalf("could not find %s in root install.sh", name)
}
return string(match)
}
func extractSelectedUpdateChannelShellFunctions(t *testing.T) string {
t.Helper()
return extractRootInstallShellFunction(t, "read_configured_update_channel") + "\n" +
extractRootInstallShellFunction(t, "selected_update_channel")
}
func extractSetupAutoUpdatesShellFunctions(t *testing.T) string {
t.Helper()
return extractSelectedUpdateChannelShellFunctions(t) + "\n" +
extractRootInstallShellFunction(t, "repo_web_url") + "\n" +
extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + "\n" +
extractRootInstallShellFunction(t, "install_auto_update_assets") + "\n" +
extractRootInstallShellFunction(t, "setup_auto_updates")
}
func prepareAutoUpdatePaths(t *testing.T, tmpDir string) (string, string, string) {
t.Helper()
autoUpdateDest := filepath.Join(tmpDir, "bin", "pulse-auto-update.sh")
servicePath := filepath.Join(tmpDir, "systemd", "pulse-update.service")
timerPath := filepath.Join(tmpDir, "systemd", "pulse-update.timer")
if err := os.MkdirAll(filepath.Dir(autoUpdateDest), 0755); err != nil {
t.Fatalf("mkdir auto-update dest dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(servicePath), 0755); err != nil {
t.Fatalf("mkdir systemd dir: %v", err)
}
return autoUpdateDest, servicePath, timerPath
}
func extractAutoUpdateFunction(t *testing.T, name string) string {
t.Helper()
content, err := os.ReadFile(filepath.Join("..", "pulse-auto-update.sh"))
if err != nil {
t.Fatalf("read pulse-auto-update.sh: %v", err)
}
pattern := regexp.MustCompile(`(?ms)^` + regexp.QuoteMeta(name) + `\(\) \{\n.*?^\}`)
match := pattern.Find(content)
if match == nil {
t.Fatalf("could not find %s in pulse-auto-update.sh", name)
}
return string(match)
}
func extractInstallShellSection(t *testing.T, startMarker string, endMarker string) string {
t.Helper()
content, err := os.ReadFile(filepath.Join("..", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
text := string(content)
start := strings.Index(text, startMarker)
if start == -1 {
t.Fatalf("could not find start marker %q in install.sh", startMarker)
}
end := strings.Index(text[start:], endMarker)
if end == -1 {
t.Fatalf("could not find end marker %q in install.sh", endMarker)
}
return text[start : start+end]
}
func TestPlainHTTPInstallAutoEnablesInsecure(t *testing.T) {
script := `
log_info() { :; }
` + extractInstallShellFunction(t, "pulse_url_uses_plain_http") + `
` + extractInstallShellFunction(t, "auto_enable_insecure_for_plain_http_url") + `
PULSE_URL="http://192.168.0.98:7655"
INSECURE="false"
auto_enable_insecure_for_plain_http_url
echo "$INSECURE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "true" {
t.Fatalf("INSECURE = %q, want true", got)
}
}
func TestHTTPSInstallKeepsInsecureDisabledByDefault(t *testing.T) {
script := `
log_info() { :; }
` + extractInstallShellFunction(t, "pulse_url_uses_plain_http") + `
` + extractInstallShellFunction(t, "auto_enable_insecure_for_plain_http_url") + `
PULSE_URL="https://pulse.example.com"
INSECURE="false"
auto_enable_insecure_for_plain_http_url
echo "$INSECURE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "false" {
t.Fatalf("INSECURE = %q, want false", got)
}
}
func TestBuildExecArgsArrayPersistsInsecureForPlainHTTPInstall(t *testing.T) {
script := `
log_info() { :; }
` + extractInstallShellFunction(t, "pulse_url_uses_plain_http") + `
` + extractInstallShellFunction(t, "auto_enable_insecure_for_plain_http_url") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args_array") + `
PULSE_URL="http://pulse.local:7655"
PULSE_TOKEN="deadbeef"
INTERVAL="30s"
ENABLE_HOST="true"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_TYPE=""
INSECURE="false"
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/token"
ENABLE_COMMANDS=""
ENROLL=""
KUBE_INCLUDE_ALL_PODS=""
KUBE_INCLUDE_ALL_DEPLOYMENTS=""
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR=""
DISK_EXCLUDES=()
auto_enable_insecure_for_plain_http_url
build_exec_args_array
printf '%s\n' "${EXEC_ARGS_ARRAY[*]}"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
if !strings.Contains(got, "--insecure") {
t.Fatalf("EXEC_ARGS_ARRAY missing --insecure: %s", got)
}
if !strings.Contains(got, "--token-file /var/lib/pulse-agent/token") {
t.Fatalf("EXEC_ARGS_ARRAY missing runtime token file: %s", got)
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("EXEC_ARGS_ARRAY preserved raw token: %s", got)
}
}
func TestBuildExecArgsWithoutTokenOmitsPersistedToken(t *testing.T) {
script := `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args_without_token") + `
PULSE_URL="https://pulse.example.com"
PULSE_TOKEN="deadbeef"
INTERVAL="30s"
ENABLE_HOST="true"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBECONFIG_PATH=""
ENABLE_PROXMOX="true"
PROXMOX_TYPE="pbs"
INSECURE="false"
ENABLE_COMMANDS=""
ENROLL=""
KUBE_INCLUDE_ALL_PODS=""
KUBE_INCLUDE_ALL_DEPLOYMENTS=""
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
DISK_EXCLUDES=("/boot pool")
build_exec_args_without_token
printf '%s\n' "$EXEC_ARGS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
if strings.Contains(got, "--token") {
t.Fatalf("EXEC_ARGS unexpectedly preserved token: %s", got)
}
if !strings.Contains(got, "--proxmox-type pbs") {
t.Fatalf("EXEC_ARGS missing proxmox type: %s", got)
}
if !strings.Contains(got, `--disk-exclude /boot\ pool`) {
t.Fatalf("EXEC_ARGS missing quoted disk exclude: %s", got)
}
}
func TestStateDirFlagIsAcceptedByInstallerParser(t *testing.T) {
script := `
fail() { echo "FAIL:$1"; exit 99; }
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
ENABLE_HOST="true"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
UNINSTALL="false"
INSECURE="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
ENABLE_COMMANDS="false"
ENROLL="false"
KUBECONFIG_PATH=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
STATE_DIR="/var/lib/pulse-agent"
CURL_CA_BUNDLE=""
NON_INTERACTIVE="false"
TOKEN_FILE_PATH=""
OUTPUT_FORMAT="text"
PREFLIGHT_ONLY="false"
set -- --state-dir /tmp/pulse-agent-state --non-interactive --url https://pulse.example.com --token deadbeef
` + extractInstallShellSection(t, "# --- Parse Arguments ---", "# Read token from file if --token-file was provided") + `
printf 'STATE_DIR=%s\nNON_INTERACTIVE=%s\nPULSE_URL=%s\n' "$STATE_DIR" "$NON_INTERACTIVE" "$PULSE_URL"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, "STATE_DIR=/tmp/pulse-agent-state") {
t.Fatalf("STATE_DIR not parsed correctly:\n%s", got)
}
if !strings.Contains(got, "NON_INTERACTIVE=true") {
t.Fatalf("NON_INTERACTIVE not parsed correctly:\n%s", got)
}
if !strings.Contains(got, "PULSE_URL=https://pulse.example.com") {
t.Fatalf("PULSE_URL not parsed correctly:\n%s", got)
}
}
func TestInstallSHCustomStateDirOwnsTokenAndEnrollmentContinuity(t *testing.T) {
stateDir := filepath.Join(t.TempDir(), "custom-state")
if err := os.MkdirAll(stateDir, 0755); err != nil {
t.Fatal(err)
}
tokenPath := filepath.Join(stateDir, "token")
runtimePath := filepath.Join(stateDir, "runtime.token")
if err := os.WriteFile(tokenPath, []byte("aaa111"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(runtimePath, []byte("runtime-one"), 0600); err != nil {
t.Fatal(err)
}
ensureToken := extractInstallShellFunction(t, "ensure_runtime_token_file")
run := func(token string) string {
t.Helper()
script := `
set -euo pipefail
PULSE_TOKEN="` + token + `"
STATE_DIR="` + stateDir + `"
ENROLL="true"
RUNTIME_TOKEN_FILE=""
RUNTIME_TOKEN_CHANGED="false"
log_info() { :; }
` + ensureToken + `
ensure_runtime_token_file "$STATE_DIR"
printf 'changed=%s runtime=%s token_file=%s\n' \
"$RUNTIME_TOKEN_CHANGED" "$([[ -f "$STATE_DIR/runtime.token" ]] && echo present || echo missing)" "$RUNTIME_TOKEN_FILE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
return string(out)
}
if got := run("aaa111"); !strings.Contains(got, "changed=false runtime=present") {
t.Fatalf("unchanged bootstrap token did not preserve enrollment runtime state:\n%s", got)
}
if got := run("bbb222"); !strings.Contains(got, "changed=true runtime=missing") {
t.Fatalf("changed bootstrap token did not force re-enrollment:\n%s", got)
}
for path, want := range map[string]os.FileMode{
stateDir: 0700,
tokenPath: 0600,
} {
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != want {
t.Fatalf("%s mode = %o, want %o", path, got, want)
}
}
}
func TestInstallSHExplicitCustomStateNeverFallsBackToDefaultInstance(t *testing.T) {
root := t.TempDir()
customState := filepath.Join(root, "custom")
defaultState := filepath.Join(root, "default")
for _, dir := range []string{customState, defaultState} {
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
}
customConn := filepath.Join(customState, "connection.env")
if err := os.WriteFile(customConn, []byte("PULSE_URL='https://custom.example'\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(defaultState, "connection.env"), []byte("PULSE_URL='https://default.example'\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(defaultState, "agent-id"), []byte("default-agent"), 0600); err != nil {
t.Fatal(err)
}
script := `
set -euo pipefail
STATE_DIR="` + customState + `"
STATE_DIR_SOURCE="explicit"
DEFAULT_STATE_DIR="` + defaultState + `"
TRUENAS_STATE_DIR="` + filepath.Join(root, "truenas") + `"
` + extractInstallShellFunction(t, "find_connection_state_file") + `
` + extractInstallShellFunction(t, "recover_agent_id_from_state_file") + `
printf 'connection=%s\n' "$(find_connection_state_file)"
rm -f "$STATE_DIR/connection.env"
printf 'fallback_connection=%s\n' "$(find_connection_state_file || true)"
printf 'fallback_agent=%s\n' "$(recover_agent_id_from_state_file || true)"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, "connection="+customConn) {
t.Fatalf("custom connection state was not preferred:\n%s", got)
}
if !strings.Contains(got, "fallback_connection=\n") || !strings.Contains(got, "fallback_agent=\n") {
t.Fatalf("explicit custom state borrowed default instance state:\n%s", got)
}
}
func TestInstallSHSavedInstallerDiscoversItsCustomStateDir(t *testing.T) {
stateDir := filepath.Join(t.TempDir(), "saved custom state")
if err := os.MkdirAll(stateDir, 0700); err != nil {
t.Fatal(err)
}
installerPath := filepath.Join(stateDir, "install.sh")
if err := os.WriteFile(installerPath, []byte("#!/usr/bin/env bash\n"), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(stateDir, "connection.env"), []byte("PULSE_URL='https://pulse.example'\n"), 0600); err != nil {
t.Fatal(err)
}
script := `
set -euo pipefail
STATE_DIR="/var/lib/pulse-agent"
STATE_DIR_SOURCE="default"
` + extractInstallShellFunction(t, "discover_state_dir_from_saved_installer") + `
discover_state_dir_from_saved_installer "` + installerPath + `"
printf 'state=%s source=%s\n' "$STATE_DIR" "$STATE_DIR_SOURCE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
resolvedStateDir, err := filepath.EvalSymlinks(stateDir)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, "state="+resolvedStateDir) || !strings.Contains(got, "source=recovered") {
t.Fatalf("saved installer did not discover adjacent custom state:\n%s", got)
}
}
func TestInstallSHDiscoversCustomStateDirFromGeneratedSystemdUnit(t *testing.T) {
for _, commandsEnabled := range []bool{false, true} {
t.Run(map[bool]string{false: "commands_disabled", true: "commands_enabled"}[commandsEnabled], func(t *testing.T) {
root := t.TempDir()
customState := filepath.Join(root, "custom state")
defaultState := filepath.Join(root, "default")
if err := os.MkdirAll(customState, 0700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(defaultState, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(customState, "token"), []byte("custom123"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(defaultState, "token"), []byte("default999"), 0600); err != nil {
t.Fatal(err)
}
commandArg := ""
if commandsEnabled {
commandArg = " --enable-commands"
}
unitPath := filepath.Join(root, "pulse-agent.service")
unit := `[Service]
ExecStart=/usr/local/bin/pulse-agent --url https://custom.example --token-file ` +
strings.ReplaceAll(filepath.Join(customState, "token"), " ", `\ `) +
` --state-dir ` + strings.ReplaceAll(customState, " ", `\ `) + commandArg + "\n"
if err := os.WriteFile(unitPath, []byte(unit), 0644); err != nil {
t.Fatal(err)
}
script := `
set -euo pipefail
AGENT_NAME="pulse-agent"
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="` + defaultState + `"
STATE_DIR_SOURCE="default"
DEFAULT_STATE_DIR="` + defaultState + `"
TRUENAS_STATE_DIR="` + filepath.Join(root, "truenas") + `"
CURL_CA_BUNDLE=""
SERVER_FINGERPRINT=""
OBSERVERS_FILE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
log_warn() { :; }
systemctl() { printf '%s\n' "` + unitPath + `"; }
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_env_stream") + `
` + extractInstallShellFunction(t, "split_recovered_shell_words") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_systemd_unit") + `
` + extractInstallShellFunction(t, "remove_agent_state_dir") + `
recover_connection_state_from_systemd_unit
printf 'state=%s source=%s url=%s token=%s commands=%s\n' \
"$STATE_DIR" "$STATE_DIR_SOURCE" "$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_COMMANDS"
remove_agent_state_dir "$STATE_DIR"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, want := range []string{
"state=" + customState,
"source=recovered",
"url=https://custom.example",
"token=custom123",
"commands=" + map[bool]string{false: "false", true: "true"}[commandsEnabled],
} {
if !strings.Contains(got, want) {
t.Fatalf("systemd discovery missing %q:\n%s", want, got)
}
}
if strings.Contains(got, "default999") {
t.Fatalf("systemd discovery borrowed default token:\n%s", got)
}
if _, err := os.Stat(customState); !os.IsNotExist(err) {
t.Fatalf("uninstall did not remove discovered custom state: %v", err)
}
if _, err := os.Stat(defaultState); err != nil {
t.Fatalf("uninstall removed the default instance instead of discovered custom state: %v", err)
}
})
}
}
func TestInstallSHDiscoversCustomStateDirFromGeneratedLaunchdPlist(t *testing.T) {
root := t.TempDir()
customState := filepath.Join(root, "custom & state")
if err := os.MkdirAll(customState, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(customState, "token"), []byte("launch123"), 0600); err != nil {
t.Fatal(err)
}
plistPath := filepath.Join(root, "com.pulse.agent.plist")
escapedState := strings.ReplaceAll(customState, "&", "&amp;")
plist := `<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/pulse-agent</string>
<string>--url</string>
<string>https://launch.example</string>
<string>--token-file</string>
<string>` + escapedState + `/token</string>
<string>--state-dir</string>
<string>` + escapedState + `</string>
<string>--enable-commands</string>
</array>
</dict></plist>`
if err := os.WriteFile(plistPath, []byte(plist), 0644); err != nil {
t.Fatal(err)
}
script := `
set -euo pipefail
PULSE_URL=""
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="false"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="/var/lib/pulse-agent"
STATE_DIR_SOURCE="default"
DEFAULT_STATE_DIR="/var/lib/pulse-agent"
TRUENAS_STATE_DIR="/data/pulse-agent"
CURL_CA_BUNDLE=""
SERVER_FINGERPRINT=""
OBSERVERS_FILE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "launchd_agent_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_launchd_plist") + `
recover_connection_state_from_launchd_plist "` + plistPath + `"
printf 'state=%s url=%s token=%s commands=%s\n' "$STATE_DIR" "$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_COMMANDS"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, want := range []string{
"state=" + customState,
"url=https://launch.example",
"token=launch123",
"commands=true",
} {
if !strings.Contains(got, want) {
t.Fatalf("launchd discovery missing %q:\n%s", want, got)
}
}
}
func TestInstallSHConnectionEnvPersistsCanonicalStateDirWithoutTokenValue(t *testing.T) {
stateDir := filepath.Join(t.TempDir(), "state")
script := `
set -euo pipefail
STATE_DIR="` + stateDir + `"
PULSE_URL="https://pulse.example.com"
PULSE_TOKEN="deadbeef"
RUNTIME_TOKEN_FILE="$STATE_DIR/token"
AGENT_ID="agent-custom"
HOSTNAME_OVERRIDE="host-custom"
REPORT_IP="192.168.1.9"
INSECURE="false"
SERVER_FINGERPRINT=""
CURL_CA_BUNDLE=""
SAVED_INSTALL_SCRIPT=""
` + extractInstallShellFunction(t, "write_connection_state_value") + `
` + extractInstallShellFunction(t, "save_connection_info") + `
curl() { return 1; }
save_connection_info "$STATE_DIR"
cat "$STATE_DIR/connection.env"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, "PULSE_STATE_DIR='"+stateDir+"'") ||
!strings.Contains(got, "PULSE_TOKEN_FILE='"+filepath.Join(stateDir, "token")+"'") {
t.Fatalf("connection.env did not persist canonical state paths:\n%s", got)
}
if !strings.Contains(got, "PULSE_REPORT_IP='192.168.1.9'") {
t.Fatalf("connection.env did not persist the report IP:\n%s", got)
}
if strings.Contains(got, "PULSE_TOKEN='") || strings.Contains(got, "deadbeef") {
t.Fatalf("connection.env leaked the token value:\n%s", got)
}
info, err := os.Stat(filepath.Join(stateDir, "connection.env"))
if err != nil {
t.Fatal(err)
}
if gotMode := info.Mode().Perm(); gotMode != 0600 {
t.Fatalf("connection.env mode = %o, want 600", gotMode)
}
}
func TestInstallSHStateWritesReplaceSymlinksAtomically(t *testing.T) {
stateDir := filepath.Join(t.TempDir(), "state")
if err := os.MkdirAll(stateDir, 0700); err != nil {
t.Fatal(err)
}
victimToken := filepath.Join(t.TempDir(), "victim-token")
victimConnection := filepath.Join(t.TempDir(), "victim-connection")
if err := os.WriteFile(victimToken, []byte("victim-token-content"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(victimConnection, []byte("victim-connection-content"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(victimToken, filepath.Join(stateDir, "token")); err != nil {
t.Fatal(err)
}
if err := os.Symlink(victimConnection, filepath.Join(stateDir, "connection.env")); err != nil {
t.Fatal(err)
}
script := `
set -euo pipefail
STATE_DIR="` + stateDir + `"
PULSE_URL="https://pulse.example.com"
IFS= read -r PULSE_TOKEN
RUNTIME_TOKEN_FILE=""
RUNTIME_TOKEN_CHANGED="false"
ENROLL="false"
AGENT_ID="agent-one"
HOSTNAME_OVERRIDE="host-one"
REPORT_IP=""
INSECURE="false"
SERVER_FINGERPRINT=""
CURL_CA_BUNDLE=""
SAVED_INSTALL_SCRIPT=""
NON_INTERACTIVE="true"
TMP_FILES=()
log_info() { :; }
curl() { return 1; }
` + extractInstallShellFunction(t, "write_connection_state_value") + `
` + extractInstallShellFunction(t, "ensure_runtime_token_file") + `
` + extractInstallShellFunction(t, "save_connection_info") + `
ensure_runtime_token_file "$STATE_DIR"
save_connection_info "$STATE_DIR"
`
cmd := exec.Command("bash", "-c", script)
cmd.Stdin = strings.NewReader("new-token-content\n")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
for path, want := range map[string]string{
victimToken: "victim-token-content",
victimConnection: "victim-connection-content",
} {
got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Fatalf("state write followed symlink and modified %s: %q", path, got)
}
}
for _, path := range []string{filepath.Join(stateDir, "token"), filepath.Join(stateDir, "connection.env")} {
info, err := os.Lstat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode()&os.ModeSymlink != 0 {
t.Fatalf("%s remained a symlink after atomic replacement", path)
}
}
}
func TestInstallSHCurlTokenTransportKeepsSecretOutOfArgv(t *testing.T) {
recordDir := t.TempDir()
argsPath := filepath.Join(recordDir, "args")
configPath := filepath.Join(recordDir, "config")
script := `
set -euo pipefail
PULSE_TOKEN="feedface"
curl() {
printf '%s\n' "$@" > "` + argsPath + `"
while [[ $# -gt 0 ]]; do
if [[ "$1" == "--config" ]]; then
cp "$2" "` + configPath + `"
shift 2
continue
fi
shift
done
}
` + extractInstallShellFunction(t, "curl_with_pulse_token") + `
curl_with_pulse_token -sS https://pulse.example.com/api/agents/agent/lookup
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
args, err := os.ReadFile(argsPath)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(args), "feedface") || strings.Contains(string(args), "X-API-Token") {
t.Fatalf("curl argv leaked token material:\n%s", args)
}
config, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(config), "X-API-Token: feedface") {
t.Fatalf("private curl config did not carry auth header:\n%s", config)
}
}
func TestSetupUpdateCommandHonorsRCChannelAndCustomPaths(t *testing.T) {
tmpDir := t.TempDir()
updatePath := filepath.Join(tmpDir, "update")
profilePath := filepath.Join(tmpDir, "profile")
bashrcPath := filepath.Join(tmpDir, "bashrc")
configDir := filepath.Join(tmpDir, "pulse-config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc"}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
if err := os.WriteFile(bashrcPath, []byte(""), 0644); err != nil {
t.Fatalf("write bashrc: %v", err)
}
script := `
PULSE_UPDATE_HELPER_PATH="` + updatePath + `"
PULSE_PROFILE_PATH="` + profilePath + `"
PULSE_BASHRC_PATH="` + bashrcPath + `"
GITHUB_REPO="example/pulse-fork"
` + extractRootInstallShellFunction(t, "setup_update_command") + `
setup_update_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(updatePath)
if err != nil {
t.Fatalf("read update helper: %v", err)
}
got := string(content)
if !strings.Contains(got, `CONFIG_DIR=/etc/pulse`) {
t.Fatalf("update helper missing config dir logic:\n%s", got)
}
if !strings.Contains(got, `helper_args=()`) || !strings.Contains(got, `helper_args=("$@")`) {
t.Fatalf("update helper missing passthrough helper args:\n%s", got)
}
if !strings.Contains(got, `-h|--help|--uninstall|--version|--rc|--pre|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)`) {
t.Fatalf("update helper missing auto-selector guard for explicit flags:\n%s", got)
}
if !strings.Contains(got, `extra_args+=("${helper_args[@]}")`) {
t.Fatalf("update helper missing forwarded helper args:\n%s", got)
}
if !strings.Contains(got, `extra_args+=(--rc)`) {
t.Fatalf("update helper missing rc channel forwarding:\n%s", got)
}
if !strings.Contains(got, `INSTALLER_URL="https://github.com/example/pulse-fork/releases/latest/download/install.sh"`) {
t.Fatalf("update helper missing configured repo installer url:\n%s", got)
}
if !strings.Contains(got, `INSTALLER_SIG_URL="${INSTALLER_URL}.sshsig"`) {
t.Fatalf("update helper missing installer signature url:\n%s", got)
}
if !strings.Contains(got, `verify_release_signature "$tmp_installer" "$tmp_signature" "downloaded Pulse installer"`) {
t.Fatalf("update helper missing signed installer verification:\n%s", got)
}
if strings.Contains(got, `curl -fsSL "$INSTALLER_URL" |`) {
t.Fatalf("update helper still pipes installer directly to bash:\n%s", got)
}
profileContent, err := os.ReadFile(profilePath)
if err != nil {
t.Fatalf("read profile: %v", err)
}
if !strings.Contains(string(profileContent), `/usr/local/bin`) {
t.Fatalf("profile not updated with /usr/local/bin path:\n%s", profileContent)
}
}
func TestSetupUpdateCommandUsesConfiguredInstallerRepo(t *testing.T) {
tmpDir := t.TempDir()
updatePath := filepath.Join(tmpDir, "update")
profilePath := filepath.Join(tmpDir, "profile")
bashrcPath := filepath.Join(tmpDir, "bashrc")
if err := os.WriteFile(bashrcPath, []byte(""), 0644); err != nil {
t.Fatalf("write bashrc: %v", err)
}
script := `
PULSE_UPDATE_HELPER_PATH="` + updatePath + `"
PULSE_PROFILE_PATH="` + profilePath + `"
PULSE_BASHRC_PATH="` + bashrcPath + `"
GITHUB_REPO="example/pulse-fork"
` + extractRootInstallShellFunction(t, "setup_update_command") + `
setup_update_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(updatePath)
if err != nil {
t.Fatalf("read update helper: %v", err)
}
got := string(content)
if strings.Contains(got, "https://github.com/rcourtman/Pulse/releases/latest/download/install.sh") {
t.Fatalf("update helper still hardcodes upstream repo:\n%s", got)
}
if !strings.Contains(got, `INSTALLER_URL="https://github.com/example/pulse-fork/releases/latest/download/install.sh"`) {
t.Fatalf("update helper missing configured installer repo:\n%s", got)
}
if !strings.Contains(got, `INSTALLER_SIG_URL="${INSTALLER_URL}.sshsig"`) {
t.Fatalf("update helper missing signature sidecar url:\n%s", got)
}
}
func TestSetupUpdateCommandFailsWhenInstallerDownloadFails(t *testing.T) {
tmpDir := t.TempDir()
updatePath := filepath.Join(tmpDir, "update")
profilePath := filepath.Join(tmpDir, "profile")
bashrcPath := filepath.Join(tmpDir, "bashrc")
curlPath := filepath.Join(tmpDir, "curl")
fakeBashPath := filepath.Join(tmpDir, "bash")
if err := os.WriteFile(bashrcPath, []byte(""), 0644); err != nil {
t.Fatalf("write bashrc: %v", err)
}
if err := os.WriteFile(curlPath, []byte("#!/usr/bin/env bash\nexit 22\n"), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
if err := os.WriteFile(fakeBashPath, []byte("#!/usr/bin/env bash\ncat >/dev/null\nexit 0\n"), 0755); err != nil {
t.Fatalf("write bash stub: %v", err)
}
script := `
PULSE_UPDATE_HELPER_PATH="` + updatePath + `"
PULSE_PROFILE_PATH="` + profilePath + `"
PULSE_BASHRC_PATH="` + bashrcPath + `"
` + extractRootInstallShellFunction(t, "setup_update_command") + `
setup_update_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
cmd := exec.Command("bash", updatePath)
cmd.Env = append(os.Environ(), "PATH="+tmpDir+string(os.PathListSeparator)+os.Getenv("PATH"))
out, err = cmd.CombinedOutput()
if err == nil {
t.Fatalf("expected generated update helper to fail when curl fails:\n%s", out)
}
if !strings.Contains(string(out), "Updating Pulse...") {
t.Fatalf("expected helper output before failure, got:\n%s", out)
}
}
func TestResolveInstallScriptDownloadURLUsesForcedVersion(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
` + extractRootInstallShellFunction(t, "resolve_install_script_download_url") + `
resolve_install_script_download_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "https://github.com/rcourtman/Pulse/releases/download/v1.2.3/install.sh"
if got != want {
t.Fatalf("download url = %q, want %q", got, want)
}
}
func TestRootInstallStableReleaseTagRejectsPrereleaseShapes(t *testing.T) {
script := `
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
if ! is_stable_release_tag v5.1.29; then
echo "rejected v-prefixed stable tag" >&2
exit 1
fi
if ! is_stable_release_tag 5.1.29; then
echo "rejected bare stable tag" >&2
exit 1
fi
if is_stable_release_tag v6.0.0-rc.2; then
echo "accepted rc tag" >&2
exit 1
fi
if is_stable_release_tag v6.0.0-beta.1; then
echo "accepted beta tag" >&2
exit 1
fi
if is_stable_release_tag latest; then
echo "accepted floating tag" >&2
exit 1
fi
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
}
func TestResolveLatestReleaseTagForStableChannelSkipsPrereleaseShapedTags(t *testing.T) {
tmpDir := t.TempDir()
timeoutPath := filepath.Join(tmpDir, "timeout")
curlPath := filepath.Join(tmpDir, "curl")
if err := os.WriteFile(timeoutPath, []byte("#!/usr/bin/env bash\nshift\nexec \"$@\"\n"), 0755); err != nil {
t.Fatalf("write timeout stub: %v", err)
}
curlStub := `#!/usr/bin/env bash
for arg in "$@"; do
if [[ "$arg" == "https://api.github.com/repos/rcourtman/Pulse/releases" ]]; then
printf '%s\n' '[{"draft":false,"prerelease":false,"tag_name":"v6.0.0-rc.2"},{"draft":false,"prerelease":false,"tag_name":"v5.1.29"}]'
exit 0
fi
done
echo "unexpected curl invocation: $*" >&2
exit 1
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
get_latest_release_from_redirect() {
printf '%s\n' v6.0.0-rc.2
}
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
` + extractRootInstallShellFunction(t, "latest_stable_release_tag_from_json") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
resolve_latest_release_tag_for_channel stable
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "v5.1.29" {
t.Fatalf("stable release = %q, want v5.1.29", got)
}
}
func TestResolveLatestReleaseTagForStableChannelRejectsPrereleaseRedirect(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
curl() { return 1; }
timeout() { shift; "$@"; }
get_latest_release_from_redirect() {
printf '%s\n' v6.0.0-rc.2
}
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
` + extractRootInstallShellFunction(t, "latest_stable_release_tag_from_json") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
if resolve_latest_release_tag_for_channel stable; then
echo "stable channel accepted prerelease redirect" >&2
exit 1
fi
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
}
func TestResolveInstallScriptDownloadURLUsesStableReleaseTag(t *testing.T) {
tmpDir := t.TempDir()
timeoutPath := filepath.Join(tmpDir, "timeout")
curlPath := filepath.Join(tmpDir, "curl")
if err := os.WriteFile(timeoutPath, []byte("#!/usr/bin/env bash\nshift\nexec \"$@\"\n"), 0755); err != nil {
t.Fatalf("write timeout stub: %v", err)
}
curlStub := `#!/usr/bin/env bash
for arg in "$@"; do
if [[ "$arg" == "https://api.github.com/repos/rcourtman/Pulse/releases" ]]; then
printf '%s\n' '[{"draft":false,"prerelease":true,"tag_name":"v6.0.0-rc.2"},{"draft":false,"prerelease":false,"tag_name":"v5.1.29"}]'
exit 0
fi
done
echo "unexpected curl invocation: $*" >&2
exit 1
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
get_latest_release_from_redirect() {
printf '%s\n' v6.0.0-rc.2
}
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
` + extractRootInstallShellFunction(t, "latest_stable_release_tag_from_json") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
` + extractRootInstallShellFunction(t, "resolve_install_script_download_url") + `
resolve_install_script_download_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "https://github.com/rcourtman/Pulse/releases/download/v5.1.29/install.sh"
if got != want {
t.Fatalf("download url = %q, want %q", got, want)
}
}
func TestResolveInstallScriptDownloadURLUsesRCReleaseTag(t *testing.T) {
tmpDir := t.TempDir()
timeoutPath := filepath.Join(tmpDir, "timeout")
curlPath := filepath.Join(tmpDir, "curl")
if err := os.WriteFile(timeoutPath, []byte("#!/usr/bin/env bash\nshift\nexec \"$@\"\n"), 0755); err != nil {
t.Fatalf("write timeout stub: %v", err)
}
curlStub := `#!/usr/bin/env bash
for arg in "$@"; do
if [[ "$arg" == "https://api.github.com/repos/rcourtman/Pulse/releases" ]]; then
printf '%s\n' '[{"draft":false,"tag_name":"v6.0.0-rc.2"},{"draft":false,"prerelease":false,"tag_name":"v5.9.0"}]'
exit 0
fi
done
echo "unexpected curl invocation: $*" >&2
exit 1
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL="rc"
UPDATE_CHANNEL=""
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
` + extractRootInstallShellFunction(t, "resolve_install_script_download_url") + `
resolve_install_script_download_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.2/install.sh"
if got != want {
t.Fatalf("download url = %q, want %q", got, want)
}
}
func TestInstallAdditionalAgentBinariesCopiesLocalExtrasWithoutNetwork(t *testing.T) {
tmpDir := t.TempDir()
sourceDir := filepath.Join(tmpDir, "source")
installDir := filepath.Join(tmpDir, "opt", "pulse")
if err := os.MkdirAll(filepath.Join(sourceDir, "bin"), 0755); err != nil {
t.Fatalf("mkdir source bin: %v", err)
}
if err := os.MkdirAll(filepath.Join(installDir, "bin"), 0755); err != nil {
t.Fatalf("mkdir install bin: %v", err)
}
agentPath := filepath.Join(sourceDir, "bin", "pulse-agent-linux-arm64")
if err := os.WriteFile(agentPath, []byte("unified-agent\n"), 0755); err != nil {
t.Fatalf("write agent: %v", err)
}
script := `
INSTALL_DIR="` + installDir + `"
GITHUB_REPO="rcourtman/Pulse"
curl_calls=0
wget_calls=0
chown() { :; }
print_info() { :; }
print_warn() { :; }
print_success() { :; }
curl() { curl_calls=$((curl_calls + 1)); return 99; }
wget() { wget_calls=$((wget_calls + 1)); return 99; }
` + extractRootInstallShellFunction(t, "copy_unified_agent_binaries_from_dir") + `
` + extractRootInstallShellFunction(t, "install_additional_agent_binaries") + `
install_additional_agent_binaries "v6.0.0-rc.3" "` + sourceDir + `"
printf 'curl=%s wget=%s\n' "$curl_calls" "$wget_calls"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "curl=0 wget=0" {
t.Fatalf("expected no network fallback calls, got %q", got)
}
if _, err := os.Stat(filepath.Join(installDir, "bin", "pulse-agent-linux-arm64")); err != nil {
t.Fatalf("expected local unified agent binary to be copied: %v", err)
}
}
func TestInstallAdditionalAgentBinariesSkipsNetworkWhenLocalExtrasAreMissing(t *testing.T) {
tmpDir := t.TempDir()
sourceDir := filepath.Join(tmpDir, "source")
installDir := filepath.Join(tmpDir, "opt", "pulse")
if err := os.MkdirAll(filepath.Join(sourceDir, "bin"), 0755); err != nil {
t.Fatalf("mkdir source bin: %v", err)
}
if err := os.MkdirAll(filepath.Join(installDir, "bin"), 0755); err != nil {
t.Fatalf("mkdir install bin: %v", err)
}
script := `
INSTALL_DIR="` + installDir + `"
GITHUB_REPO="rcourtman/Pulse"
curl_calls=0
wget_calls=0
chown() { :; }
print_info() { :; }
print_warn() { :; }
print_success() { :; }
curl() { curl_calls=$((curl_calls + 1)); return 99; }
wget() { wget_calls=$((wget_calls + 1)); return 99; }
` + extractRootInstallShellFunction(t, "copy_unified_agent_binaries_from_dir") + `
` + extractRootInstallShellFunction(t, "install_additional_agent_binaries") + `
install_additional_agent_binaries "v6.0.0-rc.3" "` + sourceDir + `"
printf 'curl=%s wget=%s\n' "$curl_calls" "$wget_calls"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "curl=0 wget=0" {
t.Fatalf("expected missing local extras to skip network fallback, got %q", got)
}
}
func TestInstallSHRequiresPinnedSignatureVerificationForReleaseDownloads(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
required := []string{
`PINNED_INSTALLER_SSH_PUBLIC_KEY="__PULSE_INSTALLER_SSH_PUBLIC_KEY__"`,
`has_pinned_installer_signature_key() {`,
`grep -i '^X-Signature-SSHSIG:' "$TMP_HEADERS"`,
`Server did not provide checksum header; refusing install.`,
`Server did not provide SSH signature header; refusing signed install.`,
`ssh-keygen -Y verify`,
`Binary signature verified`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing signed-download verification contract: %s", needle)
}
}
}
func TestBuildContainerInstallCommandPreservesForcedVersion(t *testing.T) {
script := `
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag="--enable-auto-updates"
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container --version 'v1.2.3' --enable-auto-updates"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestBuildContainerInstallCommandPreservesExplicitAutoUpdateDisable(t *testing.T) {
script := `
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag="--disable-auto-updates"
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container --version 'v1.2.3' --disable-auto-updates"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestBuildContainerInstallCommandPassesArchiveToContainer(t *testing.T) {
script := `
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
container_archive_dest="/tmp/pulse-v6.0.0-rc.1-linux-amd64.tar.gz"
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container --archive /tmp/pulse-v6.0.0-rc.1-linux-amd64.tar.gz"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestBuildContainerInstallCommandQuotesArchivePath(t *testing.T) {
script := `
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
container_archive_dest="/tmp/pulse archive-linux-amd64.tar.gz"
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := `bash /tmp/install.sh --in-container --archive /tmp/pulse\ archive-linux-amd64.tar.gz`
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestBuildContainerInstallCommandPreservesRCChannel(t *testing.T) {
script := `
FORCE_VERSION=""
FORCE_CHANNEL="rc"
UPDATE_CHANNEL=""
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7766"
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "FRONTEND_PORT=7766 bash /tmp/install.sh --in-container --rc"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestBuildContainerInstallCommandIgnoresHostConfiguredRCChannelForFreshLXCInstall(t *testing.T) {
configDir := t.TempDir()
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc"}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
script := `
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
IGNORE_CONFIGURED_UPDATE_CHANNEL="true"
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + configDir + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestPrintContainerRecoveryCommandPreservesForcedVersion(t *testing.T) {
script := `
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag="--enable-auto-updates"
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container --version 'v1.2.3' --enable-auto-updates"
if got != want {
t.Fatalf("recovery command = %q, want %q", got, want)
}
}
func TestPrintContainerRecoveryCommandPreservesExplicitAutoUpdateDisable(t *testing.T) {
script := `
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
auto_updates_flag="--disable-auto-updates"
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container --version 'v1.2.3' --disable-auto-updates"
if got != want {
t.Fatalf("recovery command = %q, want %q", got, want)
}
}
func TestPrintContainerRecoveryCommandPreservesRCChannel(t *testing.T) {
script := `
FORCE_VERSION=""
FORCE_CHANNEL="rc"
UPDATE_CHANNEL=""
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7766"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "FRONTEND_PORT=7766 bash /tmp/install.sh --in-container --rc"
if got != want {
t.Fatalf("recovery command = %q, want %q", got, want)
}
}
func TestResolveReleaseAssetBaseURLUsesLatestRelease(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
LATEST_RELEASE="v1.2.3"
` + extractRootInstallShellFunction(t, "resolve_release_asset_base_url") + `
resolve_release_asset_base_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "https://github.com/rcourtman/Pulse/releases/download/v1.2.3"
if got != want {
t.Fatalf("asset base url = %q, want %q", got, want)
}
}
func TestDownloadAutoUpdateScriptUsesSelectedReleaseAssets(t *testing.T) {
tmpDir := t.TempDir()
curlPath := filepath.Join(tmpDir, "curl")
sshKeygenPath := filepath.Join(tmpDir, "ssh-keygen")
destPath := filepath.Join(tmpDir, "pulse-auto-update.sh")
logPath := filepath.Join(tmpDir, "curl.log")
curlStub := `#!/usr/bin/env bash
set -e
out=""
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o)
out="$2"
shift 2
;;
--connect-timeout|--max-time)
shift 2
;;
-fsSL|-fsS|-fsSL)
shift
;;
*)
url="$1"
shift
;;
esac
done
printf '%s\n' "$url" >> "` + logPath + `"
case "$url" in
"https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-auto-update.sh")
printf '#!/usr/bin/env bash\nexit 0\n' > "$out"
;;
"https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-auto-update.sh.sshsig")
printf 'signed-payload\n' > "$out"
;;
*)
echo "unexpected url: $url" >&2
exit 1
;;
esac
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
sshKeygenStub := `#!/usr/bin/env bash
exit 0
`
if err := os.WriteFile(sshKeygenPath, []byte(sshKeygenStub), 0755); err != nil {
t.Fatalf("write ssh-keygen stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
LATEST_RELEASE="v9.9.9"
PULSE_AUTO_UPDATE_DEST="` + destPath + `"
print_warn() { :; }
print_info() { :; }
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
` + extractRootInstallShellFunction(t, "release_signature_key_available") + `
` + extractRootInstallShellFunction(t, "require_release_signature_verifier") + `
` + extractRootInstallShellFunction(t, "verify_release_signature") + `
` + extractRootInstallShellFunction(t, "resolve_release_asset_base_url") + `
` + extractRootInstallShellFunction(t, "download_auto_update_script") + `
download_auto_update_script
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
logContent, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read curl log: %v", err)
}
got := string(logContent)
if !strings.Contains(got, "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-auto-update.sh") {
t.Fatalf("missing versioned helper download:\n%s", got)
}
if !strings.Contains(got, "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-auto-update.sh.sshsig") {
t.Fatalf("missing versioned signature download:\n%s", got)
}
}
func TestPulseAutoUpdatePerformUpdateUsesVersionedInstallerURL(t *testing.T) {
tmpDir := t.TempDir()
curlPath := filepath.Join(tmpDir, "curl")
sshKeygenPath := filepath.Join(tmpDir, "ssh-keygen")
logPath := filepath.Join(tmpDir, "curl.log")
installDir := filepath.Join(tmpDir, "install")
if err := os.MkdirAll(filepath.Join(installDir, "bin"), 0755); err != nil {
t.Fatalf("mkdir install bin: %v", err)
}
if err := os.WriteFile(filepath.Join(installDir, "bin", "pulse"), []byte("old"), 0755); err != nil {
t.Fatalf("write fake pulse binary: %v", err)
}
if err := os.WriteFile(filepath.Join(installDir, "VERSION"), []byte("v1.0.0\n"), 0644); err != nil {
t.Fatalf("write VERSION: %v", err)
}
curlStub := `#!/usr/bin/env bash
set -e
printf '%s\n' "$*" >> "` + logPath + `"
out=""
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o)
out="$2"
shift 2
;;
-fsSL)
shift
;;
*)
url="$1"
shift
;;
esac
done
case "$url" in
"https://github.com/rcourtman/Pulse/releases/download/v9.9.9/install.sh")
printf '#!/usr/bin/env bash\nexit 0\n' > "$out"
;;
"https://github.com/rcourtman/Pulse/releases/download/v9.9.9/install.sh.sshsig")
printf 'signed-payload\n' > "$out"
;;
esac
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
if err := os.WriteFile(sshKeygenPath, []byte("#!/usr/bin/env bash\nexit 0\n"), 0755); err != nil {
t.Fatalf("write ssh-keygen stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
INSTALL_DIR="` + installDir + `"
log() { :; }
detect_service_name() { echo pulse; }
get_current_version() { echo v9.9.9; }
systemctl() { return 0; }
INSTALL_SIGNATURE_IDENTITY="pulse-installer"
INSTALL_SIGNATURE_NAMESPACE="pulse-install"
PINNED_RELEASE_SSH_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZd/DaH+BldzOkq1A8KVTcFk73nAyrE8aJOyf7i00jm pulse-installer"
` + extractAutoUpdateFunction(t, "release_signature_key_available") + `
` + extractAutoUpdateFunction(t, "require_release_signature_verifier") + `
` + extractAutoUpdateFunction(t, "verify_release_signature") + `
` + extractAutoUpdateFunction(t, "resolve_install_script_url") + `
` + extractAutoUpdateFunction(t, "is_prerelease_tag") + `
wait_for_service_active() { return 0; }
` + extractAutoUpdateFunction(t, "perform_update") + `
perform_update v9.9.9
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
logContent, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("read curl log: %v", err)
}
got := string(logContent)
if !strings.Contains(got, "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/install.sh") {
t.Fatalf("perform_update did not use versioned installer url:\n%s", got)
}
if !strings.Contains(got, "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/install.sh.sshsig") {
t.Fatalf("perform_update did not use versioned installer signature url:\n%s", got)
}
if strings.Contains(got, "releases/latest/download/install.sh") {
t.Fatalf("perform_update still used latest installer url:\n%s", got)
}
}
func TestPulseAutoUpdateResolveInstallScriptURLUsesConfiguredRepo(t *testing.T) {
script := `
GITHUB_REPO="example/pulse-fork"
` + extractAutoUpdateFunction(t, "resolve_install_script_url") + `
resolve_install_script_url v9.9.9
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/releases/download/v9.9.9/install.sh" {
t.Fatalf("resolve_install_script_url = %q", got)
}
}
func TestRepoDockerDocsURLUsesConfiguredRepo(t *testing.T) {
script := `
GITHUB_REPO="example/pulse-fork"
LATEST_RELEASE="v9.9.9"
` + extractRootInstallShellFunction(t, "repo_web_url") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
repo_docker_docs_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/blob/v9.9.9/docs/DOCKER.md" {
t.Fatalf("repo_docker_docs_url = %q", got)
}
}
func TestRepoDockerDocsURLFallsBackToReleaseLandingPageWhenVersionUnknown(t *testing.T) {
script := `
GITHUB_REPO="example/pulse-fork"
get_latest_release_from_redirect() { return 1; }
curl() { return 1; }
timeout() { return 1; }
` + extractRootInstallShellFunction(t, "repo_web_url") + `
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
` + extractRootInstallShellFunction(t, "latest_stable_release_tag_from_json") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
repo_docker_docs_url
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "https://github.com/example/pulse-fork/releases/latest" {
t.Fatalf("repo_docker_docs_url fallback = %q", got)
}
}
func TestRepoDockerImageRefUsesConfiguredImageRepo(t *testing.T) {
script := `
DOCKER_IMAGE_REPO="example/pulse-enterprise"
` + extractRootInstallShellFunction(t, "repo_docker_image_ref") + `
repo_docker_image_ref latest
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "example/pulse-enterprise:latest" {
t.Fatalf("repo_docker_image_ref = %q", got)
}
}
func TestCheckDockerEnvironmentUsesConfiguredImageAndDocs(t *testing.T) {
script := `
GITHUB_REPO="example/pulse-fork"
LATEST_RELEASE="v9.9.9"
DOCKER_IMAGE_REPO="example/pulse-enterprise"
print_error() { printf 'ERR:%s\n' "$1"; }
grep() { return 1; }
` + extractRootInstallShellFunction(t, "repo_web_url") + `
` + extractRootInstallShellFunction(t, "resolve_latest_release_tag_for_channel") + `
` + extractRootInstallShellFunction(t, "repo_release_docs_ref") + `
` + extractRootInstallShellFunction(t, "repo_docs_url_for_path") + `
` + extractRootInstallShellFunction(t, "repo_docker_docs_url") + `
` + extractRootInstallShellFunction(t, "repo_docker_image_ref") + `
` + extractRootInstallShellFunction(t, "check_docker_environment") + `
container="docker"
check_docker_environment
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err == nil {
t.Fatalf("expected docker environment check to exit non-zero:\n%s", out)
}
got := string(out)
if !strings.Contains(got, "docker run -d -p 7655:7655 example/pulse-enterprise:latest") {
t.Fatalf("docker guidance missing configured image repo:\n%s", got)
}
if !strings.Contains(got, "https://github.com/example/pulse-fork/blob/v9.9.9/docs/DOCKER.md") {
t.Fatalf("docker guidance missing configured docs url:\n%s", got)
}
}
func TestBuildPrintedManagementCommandPreservesRCChannel(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL="rc"
UPDATE_CHANNEL=""
` + extractRootInstallShellFunction(t, "build_printed_management_command") + `
build_printed_management_command update
build_printed_management_command reset
build_printed_management_command uninstall
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) != 3 {
t.Fatalf("expected 3 commands, got %d:\n%s", len(lines), out)
}
if got := lines[0]; got != "/bin/update --rc" {
t.Fatalf("update command missing rc flag: %s", lines[0])
}
if got := lines[1]; got != "/bin/update --rc --reset" {
t.Fatalf("reset command missing rc flag: %s", lines[1])
}
if strings.Contains(lines[2], "--rc") {
t.Fatalf("uninstall command should not include channel flags: %s", lines[2])
}
}
func TestRootPrintCompletionRevealsBootstrapTokenThroughCLI(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
installDir := filepath.Join(tmpDir, "install")
pulseBin := filepath.Join(tmpDir, "bin", "pulse")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(pulseBin), 0755); err != nil {
t.Fatalf("mkdir pulse bin dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, ".bootstrap_token"), []byte(`{"version":2,"token_ciphertext":"encrypted-token","token_hash":"hash"}`), 0600); err != nil {
t.Fatalf("write encrypted bootstrap token marker: %v", err)
}
if err := os.WriteFile(pulseBin, []byte(`#!/usr/bin/env bash
if [[ "$1" != "bootstrap-token" ]]; then
exit 2
fi
printf 'Token: raw-bootstrap-token\n'
printf 'Data: %s\n' "$PULSE_DATA_DIR"
`), 0755); err != nil {
t.Fatalf("write fake pulse binary: %v", err)
}
script := `
RED=''
GREEN=''
YELLOW=''
BLUE=''
NC=''
CONFIG_DIR="$TEST_CONFIG_DIR"
INSTALL_DIR="$TEST_INSTALL_DIR"
BINARY_LINK_PATH="$TEST_PULSE_BIN"
UPDATE_HELPER_PATH="/bin/update"
SERVICE_NAME="pulse"
UPDATE_TIMER_UNIT="pulse-update.timer"
hostname() { if [[ "${1:-}" == "-I" ]]; then printf '127.0.0.1\n'; else command hostname "$@"; fi; }
current_frontend_port() { printf '7655\n'; }
print_header() { :; }
print_success() { printf '%s\n' "$1"; }
print_warn() { printf 'WARN: %s\n' "$1"; }
print_info() { printf 'INFO: %s\n' "$1"; }
update_timer_exists() { return 1; }
update_timer_enabled() { return 1; }
build_printed_management_command() { printf '/bin/update\n'; }
` + extractRootInstallShellFunction(t, "print_completion") + `
print_completion
`
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(),
"TEST_CONFIG_DIR="+configDir,
"TEST_INSTALL_DIR="+installDir,
"TEST_PULSE_BIN="+pulseBin,
)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
if !strings.Contains(got, "raw-bootstrap-token") {
t.Fatalf("completion output did not include revealed token:\n%s", got)
}
if !strings.Contains(got, "Data: "+configDir) {
t.Fatalf("completion output did not pass PULSE_DATA_DIR to bootstrap-token:\n%s", got)
}
if strings.Contains(got, "encrypted-token") || strings.Contains(got, "token_ciphertext") {
t.Fatalf("completion output leaked encrypted bootstrap file contents:\n%s", got)
}
}
func TestBuildPrintedManagementCommandPreservesForcedVersion(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION="v1.2.3"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
` + extractRootInstallShellFunction(t, "build_printed_management_command") + `
build_printed_management_command update
build_printed_management_command reset
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 commands, got %d:\n%s", len(lines), out)
}
if got := lines[0]; got != "/bin/update --version v1.2.3" {
t.Fatalf("update command missing version pin: %s", lines[0])
}
if got := lines[1]; got != "/bin/update --version v1.2.3 --reset" {
t.Fatalf("reset command missing version pin: %s", lines[1])
}
}
func TestBuildPrintedManagementCommandUsesConfiguredHelperPath(t *testing.T) {
script := `
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
UPDATE_HELPER_PATH="/usr/local/bin/update-pulse-preview"
` + extractRootInstallShellFunction(t, "build_printed_management_command") + `
build_printed_management_command update
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "/usr/local/bin/update-pulse-preview" {
t.Fatalf("printed command = %q, want configured helper path", got)
}
}
func TestSelectedUpdateChannelTreatsPrereleaseVersionAsRC(t *testing.T) {
script := `
FORCE_CHANNEL=""
FORCE_VERSION="v1.2.3-rc.4"
UPDATE_CHANNEL=""
CONFIG_DIR="` + t.TempDir() + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
selected_update_channel
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "rc" {
t.Fatalf("selected_update_channel = %q, want rc", got)
}
}
func TestResolveTargetReleaseIgnoresHostConfiguredRCChannelForFreshLXCInstall(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc"}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
timeoutPath := filepath.Join(tmpDir, "timeout")
curlPath := filepath.Join(tmpDir, "curl")
if err := os.WriteFile(timeoutPath, []byte("#!/usr/bin/env bash\nshift\nexec \"$@\"\n"), 0755); err != nil {
t.Fatalf("write timeout stub: %v", err)
}
curlStub := `#!/usr/bin/env bash
for arg in "$@"; do
if [[ "$arg" == "https://api.github.com/repos/rcourtman/Pulse/releases" ]]; then
printf '%s\n' '[{"draft":false,"prerelease":false,"tag_name":"v6.0.0-rc.1"},{"draft":false,"prerelease":false,"tag_name":"v5.1.28"}]'
exit 0
fi
done
echo "unexpected curl invocation: $*" >&2
exit 1
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
IGNORE_CONFIGURED_UPDATE_CHANNEL="true"
CONFIG_DIR="` + configDir + `"
print_info() { :; }
print_warn() { :; }
get_latest_release_from_redirect() { return 1; }
` + extractRootInstallShellFunction(t, "read_configured_update_channel") + `
` + extractRootInstallShellFunction(t, "is_stable_release_tag") + `
` + extractRootInstallShellFunction(t, "latest_stable_release_tag_from_json") + `
` + extractRootInstallShellFunction(t, "resolve_target_release") + `
resolve_target_release
printf '%s\n' "$LATEST_RELEASE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "v5.1.28" {
t.Fatalf("LATEST_RELEASE = %q, want v5.1.28", got)
}
}
func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
installDir := filepath.Join(tmpDir, "install")
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil {
t.Fatalf("mkdir auto-update src dir: %v", err)
}
if err := os.WriteFile(autoUpdateSrc, []byte("#!/usr/bin/env bash\n"), 0755); err != nil {
t.Fatalf("write auto-update src: %v", err)
}
script := `
CONFIG_DIR="` + configDir + `"
INSTALL_DIR="` + installDir + `"
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
FORCE_CHANNEL="rc"
UPDATE_CHANNEL=""
GITHUB_REPO="rcourtman/Pulse"
print_info() { :; }
print_warn() { :; }
print_success() { :; }
safe_systemctl() { :; }
systemctl() { return 0; }
cp() { command cp "$@"; }
chmod() { command chmod "$@"; }
chown() { :; }
cat() { command cat "$@"; }
mkdir() { command mkdir "$@"; }
` + extractSetupAutoUpdatesShellFunctions(t) + `
setup_auto_updates
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(filepath.Join(configDir, "system.json"))
if err != nil {
t.Fatalf("read system.json: %v", err)
}
var got map[string]any
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("parse system.json: %v\n%s", err, content)
}
if enabled, ok := got["autoUpdateEnabled"].(bool); !ok || !enabled {
t.Fatalf("system.json missing autoUpdateEnabled=true:\n%s", content)
}
if channel, ok := got["updateChannel"].(string); !ok || channel != "rc" {
t.Fatalf("system.json missing updateChannel rc:\n%s", content)
}
}
func TestSetupAutoUpdatesConfiguresInstalledAutoUpdateRepo(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
systemdDir := filepath.Join(tmpDir, "systemd")
installDir := filepath.Join(tmpDir, "install")
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
autoUpdateDest := filepath.Join(tmpDir, "bin", "pulse-auto-update.sh")
servicePath := filepath.Join(systemdDir, "pulse-update.service")
timerPath := filepath.Join(systemdDir, "pulse-update.timer")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.MkdirAll(systemdDir, 0755); err != nil {
t.Fatalf("mkdir systemd dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil {
t.Fatalf("mkdir auto-update src dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(autoUpdateDest), 0755); err != nil {
t.Fatalf("mkdir auto-update dest dir: %v", err)
}
if err := os.WriteFile(autoUpdateSrc, []byte("#!/usr/bin/env bash\nGITHUB_REPO=\"rcourtman/Pulse\"\n"), 0755); err != nil {
t.Fatalf("write auto-update src: %v", err)
}
script := `
CONFIG_DIR="` + configDir + `"
INSTALL_DIR="` + installDir + `"
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
GITHUB_REPO="example/pulse-fork"
print_info() { :; }
print_warn() { :; }
print_success() { :; }
safe_systemctl() { :; }
systemctl() { return 0; }
cp() { command cp "$@"; }
chmod() { command chmod "$@"; }
chown() { :; }
cat() { command cat "$@"; }
mkdir() { command mkdir "$@"; }
mv() { command mv "$@"; }
rm() { command rm "$@"; }
awk() { command awk "$@"; }
mktemp() { command mktemp "$@"; }
` + extractSetupAutoUpdatesShellFunctions(t) + `
setup_auto_updates
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(autoUpdateDest)
if err != nil {
t.Fatalf("read configured auto-update script: %v", err)
}
got := string(content)
if strings.Contains(got, "GITHUB_REPO=\"rcourtman/Pulse\"") {
t.Fatalf("auto-update script kept upstream repo:\n%s", got)
}
if !strings.Contains(got, "GITHUB_REPO=\"example/pulse-fork\"") {
t.Fatalf("auto-update script missing configured repo:\n%s", got)
}
serviceContent, err := os.ReadFile(servicePath)
if err != nil {
t.Fatalf("read service file: %v", err)
}
if !strings.Contains(string(serviceContent), "ExecStart="+autoUpdateDest) {
t.Fatalf("service file missing configured auto-update path:\n%s", serviceContent)
}
if !strings.Contains(string(serviceContent), "Documentation=https://github.com/example/pulse-fork") {
t.Fatalf("service file missing configured documentation url:\n%s", serviceContent)
}
if _, err := os.Stat(timerPath); err != nil {
t.Fatalf("timer file missing: %v", err)
}
timerContent, err := os.ReadFile(timerPath)
if err != nil {
t.Fatalf("read timer file: %v", err)
}
if !strings.Contains(string(timerContent), "Documentation=https://github.com/example/pulse-fork") {
t.Fatalf("timer file missing configured documentation url:\n%s", timerContent)
}
}
func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
installDir := filepath.Join(tmpDir, "install")
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil {
t.Fatalf("mkdir auto-update src dir: %v", err)
}
if err := os.WriteFile(autoUpdateSrc, []byte("#!/usr/bin/env bash\n"), 0755); err != nil {
t.Fatalf("write auto-update src: %v", err)
}
script := `
CONFIG_DIR="` + configDir + `"
INSTALL_DIR="` + installDir + `"
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
FORCE_CHANNEL=""
FORCE_VERSION="v1.2.3-rc.4"
UPDATE_CHANNEL=""
GITHUB_REPO="rcourtman/Pulse"
print_info() { :; }
print_warn() { :; }
print_success() { :; }
safe_systemctl() { :; }
systemctl() { return 0; }
chown() { :; }
` + extractSetupAutoUpdatesShellFunctions(t) + `
setup_auto_updates
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(filepath.Join(configDir, "system.json"))
if err != nil {
t.Fatalf("read system.json: %v", err)
}
var got map[string]any
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("parse system.json: %v\n%s", err, content)
}
if channel, ok := got["updateChannel"].(string); !ok || channel != "rc" {
t.Fatalf("prerelease version did not persist rc channel:\n%s", content)
}
}
func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
installDir := filepath.Join(tmpDir, "install")
autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh")
autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir)
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil {
t.Fatalf("mkdir auto-update src dir: %v", err)
}
if err := os.WriteFile(autoUpdateSrc, []byte("#!/usr/bin/env bash\n"), 0755); err != nil {
t.Fatalf("write auto-update src: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc","autoUpdateEnabled":false}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
script := `
CONFIG_DIR="` + configDir + `"
INSTALL_DIR="` + installDir + `"
PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `"
PULSE_UPDATE_SERVICE_PATH="` + servicePath + `"
PULSE_UPDATE_TIMER_PATH="` + timerPath + `"
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
GITHUB_REPO="rcourtman/Pulse"
print_info() { :; }
print_warn() { :; }
print_success() { :; }
safe_systemctl() { :; }
systemctl() { return 0; }
command -v jq >/dev/null 2>&1 || true
chown() { :; }
` + extractSetupAutoUpdatesShellFunctions(t) + `
setup_auto_updates
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
content, err := os.ReadFile(filepath.Join(configDir, "system.json"))
if err != nil {
t.Fatalf("read system.json: %v", err)
}
var got map[string]any
if err := json.Unmarshal(content, &got); err != nil {
t.Fatalf("parse system.json: %v\n%s", err, content)
}
if enabled, ok := got["autoUpdateEnabled"].(bool); !ok || !enabled {
t.Fatalf("system.json missing enabled flag:\n%s", content)
}
if channel, ok := got["updateChannel"].(string); !ok || channel != "rc" {
t.Fatalf("system.json lost rc channel:\n%s", content)
}
}
// TestInstallSHVerifyAgentServerRegistrationDetectsRejectedToken verifies that
// the post-install health check tells a rejected token (401/403) apart from a
// transient "agent has not reported yet" state, so the installer can surface an
// actionable error instead of leaving a silent 401 loop behind (issue #1515).
func TestInstallSHVerifyAgentServerRegistrationDetectsRejectedToken(t *testing.T) {
urlEncode := extractInstallShellFunction(t, "url_encode")
curlWithPulseToken := extractInstallShellFunction(t, "curl_with_pulse_token")
verifyFn := extractInstallShellFunction(t, "verify_agent_server_registration")
cases := []struct {
name string
status int
body string
wantRC string
}{
{"rejected token 401", http.StatusUnauthorized, `{"error":"Authentication required"}`, "rc=2"},
{"rejected token 403", http.StatusForbidden, `{"error":"missing_scope"}`, "rc=2"},
{"registered", http.StatusOK, `{"success":true,"agent":{"id":"agent-omv"}}`, "rc=0"},
{"not reported yet", http.StatusNotFound, `{"error":"agent_not_found"}`, "rc=1"},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api/agents/agent/lookup") {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.WriteHeader(tc.status)
_, _ = w.Write([]byte(tc.body))
}))
defer server.Close()
script := `
PULSE_URL="` + server.URL + `"
PULSE_TOKEN="stale-token"
HOSTNAME_OVERRIDE="omv"
INSECURE="false"
CURL_CA_BUNDLE=""
` + curlWithPulseToken + `
` + urlEncode + `
` + verifyFn + `
verify_agent_server_registration
echo "rc=$?"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if !strings.Contains(string(out), tc.wantRC) {
t.Fatalf("case %q: want %s, got:\n%s", tc.name, tc.wantRC, out)
}
})
}
}
// TestInstallSHWarnAgentTokenRejectedIsActionable pins the actionable recovery
// copy the installer prints when the server rejects the agent's token.
func TestInstallSHWarnAgentTokenRejectedIsActionable(t *testing.T) {
logWarn := extractInstallShellFunction(t, "log_warn")
warnFn := extractInstallShellFunction(t, "warn_agent_token_rejected")
script := `
NON_INTERACTIVE="false"
redact_token() { printf '%s' "$1"; }
` + logWarn + `
` + warnFn + `
warn_agent_token_rejected
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
for _, needle := range []string{
"rejected this agent's API token",
"Re-run the full agent install command",
"mint a fresh token",
} {
if !strings.Contains(string(out), needle) {
t.Fatalf("warn_agent_token_rejected missing %q:\n%s", needle, out)
}
}
}