Pulse/scripts/installtests/install_sh_test.go
rcourtman c2f6848006 Keep Docker-in-LXC working when commands are enabled after install
lxc-attach into an unprivileged guest writes /proc/<pid>/uid_map, which
needs CAP_SETUID in the parent user namespace. NoNewPrivileges drops
CAP_SETUID from the effective set and also stops lxc-attach falling back
to the setuid newuidmap/newgidmap helpers, so the socket probe dies with
"write_id_mapping: 61 Operation not permitted".

install.sh already relaxed NoNewPrivileges for this, but only when the
agent was installed with --enable-commands. Command execution is also
togglable from the server afterwards: applyRemoteConfig starts the
command client without rewriting the unit. An agent installed without
the flag and switched on later therefore ends up able to run commands
and unable to attach to unprivileged guests, so Docker inside every
unprivileged LXC disappears from the Proxmox page. The probe failure is
logged at debug level and retried on every poll, so the surface looks
empty rather than broken while the agent re-probes the whole guest list.

Grant CAP_SETUID/CAP_SETGID to any PVE agent rather than gating on the
install-time flag, so the later toggle lands on a unit that can attach.
Ambient capabilities restore exactly the privilege lxc-attach needs and
leave the rest of the sandbox intact; the existing install-time
relaxation is unchanged.

Verified on a live PVE node. With the hardened unit the probe succeeded
only on the three privileged guests and failed on every unprivileged
one. After the ambient grant CapEff regained CAP_SETUID and both
unprivileged Docker guests were discovered, taking that node from one
Docker LXC to three.
2026-08-20 11:05:39 +01:00

5533 lines
185 KiB
Go

package installtests
import (
"encoding/json"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
)
func TestContainerAgentCompatibilityWrapperDefaultsToUnifiedHostAndDockerMonitoring(t *testing.T) {
wrapperBytes, err := os.ReadFile(repoFile("scripts", "install-container-agent.sh"))
if err != nil {
t.Fatalf("read container-agent compatibility wrapper: %v", err)
}
wrapper := string(wrapperBytes)
if !strings.Contains(wrapper, `forward_args+=(--enable-docker --enable-host)`) {
t.Fatal("container-agent compatibility wrapper must preserve host telemetry while enabling Docker")
}
if strings.Contains(wrapper, `forward_args+=(--enable-docker --disable-host)`) {
t.Fatal("container-agent compatibility wrapper must not silently force workload-only mode")
}
if !strings.Contains(wrapper, `--enable-host=false for an intentional`) {
t.Fatal("container-agent compatibility wrapper must document the explicit workload-only path")
}
}
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" ]]; 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)
}
}
}
// A PVE agent installed without --enable-commands must still be provisioned for
// lxc-attach, because command execution can be enabled later from the server
// without rewriting the unit. Without CAP_SETUID, lxc-attach cannot write
// /proc/<pid>/uid_map for unprivileged guests, so Docker inside every
// unprivileged LXC silently disappears from the Proxmox page.
func TestInstallSHGrantsLXCAttachCapabilitiesForAnyPVEAgent(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_may_attach_lxc() {`,
`if [[ "$ENABLE_PROXMOX" != "true" ]]; then`,
`if systemd_agent_may_attach_lxc; then`,
`AmbientCapabilities=CAP_SETUID CAP_SETGID`,
`SystemCallArchitectures=native${ambient_line}`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing unprivileged-LXC attach capability grant: %s", needle)
}
}
if strings.Contains(script, `systemd_agent_may_attach_lxc`) &&
!strings.Contains(script, "command execution can be turned on later") {
t.Fatal("install.sh must document why the capability grant is not gated on ENABLE_COMMANDS")
}
}
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 TestInstallSHPreflightFollowsAgentDownloadRedirect(t *testing.T) {
var requestedArtifactMethod 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":
w.Header().Set("X-Checksum-Sha256", strings.Repeat("a", 64))
http.Redirect(w, r, "/artifacts/pulse-agent?"+r.URL.RawQuery, http.StatusTemporaryRedirect)
case "/artifacts/pulse-agent":
requestedArtifactMethod = r.Method
w.Header().Set("X-Checksum-Sha256", strings.Repeat("b", 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("redirected preflight failed: %v\n%s", err, out)
}
if got := string(out); !strings.Contains(got, `"code":"agent_download_available"`) {
t.Fatalf("redirected preflight did not report agent download availability:\n%s", got)
}
if requestedArtifactMethod != http.MethodHead {
t.Fatalf("redirected artifact method = %q, want HEAD", requestedArtifactMethod)
}
}
func TestInstallSHPreflightRejectsChecksumOnlyOnRedirectResponse(t *testing.T) {
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":
w.Header().Set("X-Checksum-Sha256", strings.Repeat("a", 64))
http.Redirect(w, r, "/artifacts/pulse-agent?"+r.URL.RawQuery, http.StatusTemporaryRedirect)
case "/artifacts/pulse-agent":
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 accepted checksum metadata from an intermediate redirect:\n%s", out)
}
if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 12 {
t.Fatalf("redirect-only checksum exit = %v, want 12\n%s", err, out)
}
if got := string(out); !strings.Contains(got, `"code":"agent_download_checksum_missing"`) {
t.Fatalf("preflight did not report missing final-response checksum metadata:\n%s", got)
}
}
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=(-fsSIL --connect-timeout 5 --max-time 30 -D "$PREFLIGHT_HEADERS" -o /dev/null)`,
`final_response_header_value "$PREFLIGHT_HEADERS" "X-Checksum-Sha256"`,
`"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")
DISK_INCLUDES=("/var/log")
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>`,
`<string>--disk-include</string>`,
`<string>/var/log</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|-disk-include)`,
`--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=()
DISK_INCLUDES=()
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
--disk-exclude
/dev/sda
--disk-exclude=/var/run/samba/fd
--disk-include
/var/log
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",
"--disk-exclude /dev/sda",
"--disk-exclude /var/run/samba/fd",
"--disk-include /var/log",
}
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" "$QNAP_LOG_DIR" "$STATE_DIR"`,
`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 ${AGENT_LOG_FILE}"`,
}
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 TestInstallSHAgentDiskHeadroomRejectsSharedLowSpaceFilesystem(t *testing.T) {
script := `
set -euo pipefail
log_error() { :; }
log_info() { :; }
log_warn() { :; }
INSTALL_DIR="/usr/local/bin"
AGENT_MIN_TEMP_FREE_BYTES=$((100 * 1024))
AGENT_MIN_INSTALL_FREE_BYTES=$((80 * 1024))
` + extractInstallShellFunction(t, "bytes_to_human") + `
` + extractInstallShellFunction(t, "nearest_existing_dir") + `
` + extractInstallShellFunction(t, "get_available_bytes_for_path") + `
` + extractInstallShellFunction(t, "get_filesystem_device_for_path") + `
` + extractInstallShellFunction(t, "ensure_agent_disk_headroom") + `
df() {
if [[ "$1" == "-Pk" ]]; then
printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n'
printf '/dev/shared 1000 0 150 0%% /\n'
return 0
fi
command df "$@"
}
if ensure_agent_disk_headroom /tmp /usr/local/bin; then
echo "ensure_agent_disk_headroom unexpectedly passed on a shared full filesystem" >&2
exit 1
fi
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
}
func TestInstallSHAgentDiskHeadroomAcceptsSeparateFilesystems(t *testing.T) {
script := `
set -euo pipefail
log_error() { :; }
log_info() { :; }
log_warn() { :; }
INSTALL_DIR="/usr/local/bin"
AGENT_MIN_TEMP_FREE_BYTES=$((100 * 1024))
AGENT_MIN_INSTALL_FREE_BYTES=$((80 * 1024))
` + extractInstallShellFunction(t, "bytes_to_human") + `
` + extractInstallShellFunction(t, "nearest_existing_dir") + `
` + extractInstallShellFunction(t, "get_available_bytes_for_path") + `
` + extractInstallShellFunction(t, "get_filesystem_device_for_path") + `
` + extractInstallShellFunction(t, "ensure_agent_disk_headroom") + `
df() {
if [[ "$1" == "-Pk" ]]; then
case "$2" in
/tmp)
printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n'
printf '/dev/tmp 1000 0 120 0%% /tmp\n'
return 0
;;
*)
printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\n'
printf '/dev/root 1000 0 90 0%% /\n'
return 0
;;
esac
fi
command df "$@"
}
ensure_agent_disk_headroom /tmp /usr/local/bin
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
}
func TestInstallSHAgentDiskHeadroomResolvesMissingDirectories(t *testing.T) {
script := `
set -euo pipefail
log_error() { :; }
log_info() { :; }
log_warn() { :; }
INSTALL_DIR="/usr/local/bin"
AGENT_MIN_TEMP_FREE_BYTES=$((100 * 1024))
AGENT_MIN_INSTALL_FREE_BYTES=$((80 * 1024))
` + extractInstallShellFunction(t, "nearest_existing_dir") + `
resolved=$(nearest_existing_dir /definitely/not/a/real/path/anywhere)
if [[ "$resolved" != "/" ]]; then
echo "nearest_existing_dir resolved to $resolved, want /" >&2
exit 1
fi
resolved=$(nearest_existing_dir /tmp)
if [[ "$resolved" != "/tmp" ]]; then
echo "nearest_existing_dir resolved to $resolved, want /tmp" >&2
exit 1
fi
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
}
// TestInstallSHChecksDiskHeadroomBeforeDownload pins the ENOSPC preflight from
// issue #1617: the installer must verify temp and install-dir headroom before
// the agent binary download stages anything, and the --preflight-only mode must
// report the same check.
func TestInstallSHChecksDiskHeadroomBeforeDownload(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
headroomCall := `if ! ensure_agent_disk_headroom "${TMPDIR:-/tmp}" "$INSTALL_DIR"; then`
stagingCall := `TMP_BIN=$(mktemp)`
headroomPos := strings.Index(script, headroomCall)
if headroomPos < 0 {
t.Fatalf("install.sh does not gate the download on ensure_agent_disk_headroom")
}
stagingPos := strings.Index(script, stagingCall)
if stagingPos < 0 {
t.Fatalf("install.sh no longer stages the download via mktemp; update this test")
}
if headroomPos > stagingPos {
t.Fatalf("disk headroom check happens after the download staging mktemp")
}
for _, needle := range []string{
`json_event "preflight" "disk_ok" "Sufficient disk space for agent install"`,
`json_event "preflight" "disk_low"`,
} {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh preflight-only mode missing disk headroom reporting: %s", needle)
}
}
}
// TestInstallSHWatchdogPathsUseRotatingAgentLog pins the logging half of issue
// #1617: the QNAP and Unraid watchdog loops must not shell-append agent stdout
// to an unrotated file on the RAM-backed root; the agent's own rotating writer
// (--log-file) owns the agent log instead.
func TestInstallSHWatchdogPathsUseRotatingAgentLog(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 [[ -n "${AGENT_LOG_FILE:-}" ]]; then EXEC_ARG_ITEMS+=(--log-file "$AGENT_LOG_FILE"); fi`,
`QNAP_LOG_DIR="${STATE_DIR}/logs"`,
`AGENT_LOG_FILE="${QNAP_LOG_DIR}/${AGENT_NAME}.log"`,
`UNRAID_LOG_DIR="/var/log/${AGENT_NAME}"`,
`AGENT_LOG_FILE="${UNRAID_LOG_DIR}/${AGENT_NAME}.log"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing rotating agent log wiring: %s", needle)
}
}
if got := strings.Count(script, `${EXEC_ARGS} > /dev/null 2>> "\$WATCHDOG_LOG"`); got != 2 {
t.Fatalf("expected both watchdog loops (QNAP, Unraid) to discard the stdout mirror and capture stderr in the watchdog log, found %d", got)
}
if strings.Contains(script, `${EXEC_ARGS} >> /var/log/${AGENT_NAME}.log`) {
t.Fatalf("a watchdog loop still shell-appends agent output to the unrotated /var/log/${AGENT_NAME}.log")
}
}
// TestInstallSHQNAPWatchdogIsSingleton pins the process-ownership half of issue
// #1617. QNAP can launch the persistent wrapper both from autorun.sh and from an
// install/upgrade, so the wrapper must elect one supervisor before killing or
// starting an agent and must clean up only state that it owns.
func TestInstallSHQNAPWatchdogIsSingleton(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{
`WATCHDOG_PIDFILE="${state_dir}/${AGENT_NAME}.watchdog.pid"`,
`AGENT_PIDFILE="${state_dir}/${AGENT_NAME}.pid"`,
`LOCK_DIR="${state_dir}/${AGENT_NAME}.watchdog.lock"`,
`while ! mkdir "\$LOCK_DIR" 2>/dev/null; do`,
`if pid_is_live "\$_owner"; then`,
`if ! acquire_watchdog_lock; then`,
`[ "\$(cat "\$WATCHDOG_PIDFILE" 2>/dev/null)" = "\$\$" ]`,
`trap cleanup_watchdog EXIT`,
`trap shutdown_watchdog INT TERM HUP`,
`CURRENT_AGENT_PID=\$!`,
`echo "\$CURRENT_AGENT_PID" > "\$AGENT_PIDFILE"`,
`wait "\$CURRENT_AGENT_PID"`,
}
for _, needle := range required {
if !strings.Contains(script, needle) {
t.Fatalf("install.sh missing QNAP watchdog singleton contract: %s", needle)
}
}
lockPos := strings.Index(script, `if ! acquire_watchdog_lock; then`)
killPos := strings.Index(script, `pkill -x "pulse-agent" 2>/dev/null || true`)
if lockPos < 0 || killPos < 0 || lockPos > killPos {
t.Fatalf("QNAP wrapper must acquire singleton ownership before killing an existing agent")
}
}
func TestRenderedQNAPWatchdogRejectsASecondSupervisor(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
start := strings.Index(script, "write_qnap_wrapper_script() {")
if start < 0 {
t.Fatal("install.sh missing QNAP wrapper renderer")
}
endOffset := strings.Index(script[start:], "\nappend_qnap_autorun_block() {")
if endOffset < 0 {
t.Fatal("could not isolate QNAP wrapper renderer")
}
renderer := script[start : start+endOffset]
tempDir := t.TempDir()
stateDir := filepath.Join(tempDir, "state")
logDir := filepath.Join(stateDir, "logs")
wrapperPath := filepath.Join(stateDir, "start-pulse-agent.sh")
storedPath := filepath.Join(stateDir, "stored-agent")
runtimePath := filepath.Join(tempDir, "runtime", "pulse-agent")
startsPath := filepath.Join(tempDir, "agent-starts")
mockBinDir := filepath.Join(tempDir, "bin")
for _, dir := range []string{stateDir, mockBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("create test directory: %v", err)
}
}
agentScript := "#!/bin/sh\n" +
"echo \"$$\" >> " + strconv.Quote(startsPath) + "\n" +
"trap 'exit 0' INT TERM HUP\n" +
"while :; do sleep 1; done\n"
if err := os.WriteFile(storedPath, []byte(agentScript), 0o755); err != nil {
t.Fatalf("write fake agent: %v", err)
}
if err := os.WriteFile(filepath.Join(mockBinDir, "pkill"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write fake pkill: %v", err)
}
harness := renderer + `
AGENT_NAME=pulse-agent
SHELL_EXPORT_LINES=""
EXEC_ARGS=""
write_qnap_wrapper_script "$1" "$2" "$3" "$4" "$5"
`
render := exec.Command("bash", "-c", harness, "_", wrapperPath, runtimePath, storedPath, logDir, stateDir)
if output, err := render.CombinedOutput(); err != nil {
t.Fatalf("render QNAP wrapper: %v\n%s", err, output)
}
env := []string{"PATH=" + mockBinDir + ":/usr/bin:/bin"}
first := exec.Command("sh", wrapperPath)
first.Env = env
if err := first.Start(); err != nil {
t.Fatalf("start first watchdog: %v", err)
}
t.Cleanup(func() {
if first.Process != nil {
_ = first.Process.Signal(syscall.SIGTERM)
_, _ = first.Process.Wait()
}
})
watchdogPIDFile := filepath.Join(stateDir, "pulse-agent.watchdog.pid")
agentPIDFile := filepath.Join(stateDir, "pulse-agent.pid")
waitForFile := func(path string) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if _, err := os.Stat(path); err == nil {
return
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("timed out waiting for %s", path)
}
waitForFile(watchdogPIDFile)
waitForFile(agentPIDFile)
waitForFile(startsPath)
second := exec.Command("sh", wrapperPath)
second.Env = env
if output, err := second.CombinedOutput(); err != nil {
t.Fatalf("second watchdog should exit cleanly: %v\n%s", err, output)
}
starts, err := os.ReadFile(startsPath)
if err != nil {
t.Fatalf("read fake-agent starts: %v", err)
}
if got := len(strings.Fields(string(starts))); got != 1 {
t.Fatalf("expected one supervised agent after two watchdog launches, got %d", got)
}
agentPIDBytes, err := os.ReadFile(agentPIDFile)
if err != nil {
t.Fatalf("read agent pid: %v", err)
}
agentPID, err := strconv.Atoi(strings.TrimSpace(string(agentPIDBytes)))
if err != nil {
t.Fatalf("parse agent pid: %v", err)
}
if err := first.Process.Signal(syscall.SIGTERM); err != nil {
t.Fatalf("stop first watchdog: %v", err)
}
if err := first.Wait(); err != nil {
t.Fatalf("wait for first watchdog shutdown: %v", err)
}
first.Process = nil
if err := exec.Command("kill", "-0", strconv.Itoa(agentPID)).Run(); err == nil {
t.Fatalf("watchdog shutdown left agent pid %d alive", agentPID)
}
for _, path := range []string{watchdogPIDFile, agentPIDFile, filepath.Join(stateDir, "pulse-agent.watchdog.lock")} {
if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("watchdog shutdown left singleton state %s (stat: %v)", path, err)
}
}
}
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=""`,
`system_docker_runtime_is_active() {`,
`discover_rootless_container_runtime() {`,
// Issue #1647: a live rootful Docker must win before any rootless
// socket discovery pins podman variables into the service environment.
`if system_docker_runtime_is_active; then`,
`discover_single_socket_match "${rootless_root}/*/docker.sock"`,
`discover_single_socket_match "${rootless_root}/*/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"
append_service_env "PULSE_DOCKER_RUNTIME" "podman"
append_service_env "CONTAINER_HOST" "unix:///run/user/1000/podman/podman.sock"
append_service_env "PODMAN_HOST" "unix:///run/user/1000/podman/podman.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>`,
`Environment="PULSE_DOCKER_RUNTIME=podman"`,
`Environment="CONTAINER_HOST=unix:///run/user/1000/podman/podman.sock"`,
`Environment="PODMAN_HOST=unix:///run/user/1000/podman/podman.sock"`,
}
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)
}
}
// TestInstallSHIssue1647RootfulDockerOutranksRootlessPodmanSocket pins the fix
// for issue #1647: a transient rootless Podman API socket (socket-activated for
// a login session) must not win runtime discovery while a rootful Docker daemon
// is alive, or the agent service gets podman variables pinned to a socket that
// disappears when the session ends.
func TestInstallSHIssue1647RootfulDockerOutranksRootlessPodmanSocket(t *testing.T) {
tmpDir, err := os.MkdirTemp("/tmp", "pulse-1647-*")
if err != nil {
t.Fatalf("mktemp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
podmanDir := filepath.Join(tmpDir, "run", "user", "1000", "podman")
if err := os.MkdirAll(podmanDir, 0o755); err != nil {
t.Fatalf("mkdir podman socket dir: %v", err)
}
socketPath := filepath.Join(podmanDir, "podman.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatalf("listen unix socket: %v", err)
}
defer listener.Close()
common := `
set -uo pipefail
log_warn() { :; }
log_info() { :; }
uname() { echo Linux; }
PULSE_ROOTLESS_RUNTIME_ROOT="` + filepath.Join(tmpDir, "run", "user") + `"
PULSE_SYSTEM_DOCKER_SOCKET="` + filepath.Join(tmpDir, "absent", "docker.sock") + `"
` + extractInstallShellFunction(t, "system_docker_runtime_is_active") + `
` + extractInstallShellFunction(t, "discover_single_socket_match") + `
` + extractInstallShellFunction(t, "discover_rootless_container_runtime") + `
`
liveDocker := common + `
docker() { return 0; }
if discover_rootless_container_runtime; then
echo "rootless podman socket must not outrank a live rootful docker" >&2
exit 1
fi
`
out, err := exec.Command("bash", "-c", liveDocker).CombinedOutput()
if err != nil {
t.Fatalf("live rootful docker case: %v\n%s", err, out)
}
deadDocker := common + `
docker() { return 1; }
if ! discover_rootless_container_runtime; then
echo "expected rootless podman discovery when no rootful docker answers" >&2
exit 1
fi
if [[ "$ROOTLESS_RUNTIME_KIND" != "podman" ]]; then
echo "kind=$ROOTLESS_RUNTIME_KIND, want podman" >&2
exit 1
fi
if [[ "$ROOTLESS_RUNTIME_SOCKET_URI" != "unix://` + socketPath + `" ]]; then
echo "uri=$ROOTLESS_RUNTIME_SOCKET_URI" >&2
exit 1
fi
`
out, err = exec.Command("bash", "-c", deadDocker).CombinedOutput()
if err != nil {
t.Fatalf("no rootful docker case: %v\n%s", err, out)
}
}
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 ${AGENT_LOG_FILE}"`,
`complete_installation_flow "$STATE_DIR" "Installation complete! Agent is running." "Upgrade complete! Agent is running." "tail -f ${AGENT_LOG_FILE}"`,
`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" +
extractInstallAutoUpdateAssetsShellFunctions(t) + "\n" +
extractRootInstallShellFunction(t, "setup_auto_updates")
}
// install_auto_update_assets delegates its writability probe, its staged-helper
// sanity check and the sandbox-escape migration to sibling functions, so every
// harness that executes it has to pull those in alongside it.
func extractInstallAutoUpdateAssetsShellFunctions(t *testing.T) string {
t.Helper()
return extractRootInstallShellFunction(t, "auto_update_dir_writable") + "\n" +
extractRootInstallShellFunction(t, "auto_update_helper_is_sane") + "\n" +
extractRootInstallShellFunction(t, "migrate_auto_update_assets_outside_sandbox") + "\n" +
extractRootInstallShellFunction(t, "install_auto_update_assets")
}
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("#!/bin/sh\nexit 22\n"), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
if err := os.WriteFile(fakeBashPath, []byte("#!/bin/sh\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() {`,
`final_response_header_value "$TMP_HEADERS" "X-Signature-SSHSIG"`,
`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"},
{"previous hostname owner during binding", http.StatusForbidden, `{"error":{"code":"agent_lookup_forbidden","message":"Agent does not belong to this API token"}}`, "rc=1"},
{"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"
AGENT_ID=""
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)
}
})
}
}
func TestInstallSHVerifyAgentServerRegistrationPrefersCanonicalAgentID(t *testing.T) {
urlEncode := extractInstallShellFunction(t, "url_encode")
curlWithPulseToken := extractInstallShellFunction(t, "curl_with_pulse_token")
verifyFn := extractInstallShellFunction(t, "verify_agent_server_registration")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Query().Get("id"); got != "agent-current" {
t.Errorf("lookup id = %q, want canonical agent ID", got)
}
if got := r.URL.Query().Get("hostname"); got != "" {
t.Errorf("hostname lookup = %q, want ID-only lookup", got)
}
_, _ = w.Write([]byte(`{"success":true,"agent":{"id":"agent-current"}}`))
}))
defer server.Close()
script := `
PULSE_URL="` + server.URL + `"
PULSE_TOKEN="install-token"
AGENT_ID="agent-current"
HOSTNAME_OVERRIDE="hostname-owned-by-previous-token"
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), "rc=0") {
t.Fatalf("canonical ID lookup failed:\n%s", out)
}
}
// TestInstallSHRegistrationRetryWindowOutlastsFirstReportCycle verifies that
// the post-install registration check polls for a window instead of warning
// after a single lookup: /readyz flips before the agent's first report cycle
// completes, so an immediate one-shot lookup routinely misses a healthy
// registration (issue #1644). A rejected token still short-circuits because
// more polling cannot change a definitive 401/403.
func TestInstallSHRegistrationRetryWindowOutlastsFirstReportCycle(t *testing.T) {
urlEncode := extractInstallShellFunction(t, "url_encode")
curlWithPulseToken := extractInstallShellFunction(t, "curl_with_pulse_token")
verifyFn := extractInstallShellFunction(t, "verify_agent_server_registration")
retryFn := extractInstallShellFunction(t, "verify_agent_server_registration_with_retry")
verifyStarted := extractInstallShellFunction(t, "verify_agent_started")
if !strings.Contains(verifyStarted, "verify_agent_server_registration_with_retry") {
t.Fatal("verify_agent_started does not use the registration retry window")
}
cases := []struct {
name string
// statuses is consumed one per lookup; the last value repeats.
statuses []int
wantRC string
}{
{"registered after report cycle", []int{http.StatusNotFound, http.StatusNotFound, http.StatusOK}, "rc=0"},
{"rejected token short-circuits", []int{http.StatusUnauthorized}, "rc=2"},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
var mu sync.Mutex
lookups := 0
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
}
mu.Lock()
idx := lookups
lookups++
mu.Unlock()
if idx >= len(tc.statuses) {
idx = len(tc.statuses) - 1
}
w.WriteHeader(tc.statuses[idx])
if tc.statuses[idx] == http.StatusOK {
_, _ = w.Write([]byte(`{"success":true,"agent":{"id":"agent-1644"}}`))
} else {
_, _ = w.Write([]byte(`{"error":"agent_not_found"}`))
}
}))
defer server.Close()
script := `
PULSE_URL="` + server.URL + `"
PULSE_TOKEN="install-token"
AGENT_ID=""
HOSTNAME_OVERRIDE="pve-1644"
INSECURE="false"
CURL_CA_BUNDLE=""
sleep() { :; }
` + curlWithPulseToken + `
` + urlEncode + `
` + verifyFn + `
` + retryFn + `
verify_agent_server_registration_with_retry
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)
}
if tc.wantRC == "rc=2" {
mu.Lock()
got := lookups
mu.Unlock()
if got != 1 {
t.Fatalf("rejected token was retried %d times, want 1 lookup", got)
}
}
})
}
}
func TestInstallSHRejectedCredentialPreventsSuccessfulCompletion(t *testing.T) {
completeFlow := extractInstallShellFunction(t, "complete_installation_flow")
script := `
EXIT_AUTH_REJECTED=18
UPGRADE_MODE="true"
save_connection_info() { :; }
verify_agent_started() { return 2; }
report_proxmox_registration_outcome() { printf 'unexpected-proxmox-report\n'; }
log_info() { printf 'INFO:%s\n' "$*"; }
log_warn() { printf 'WARN:%s\n' "$*"; }
log_error() { printf 'ERROR:%s\n' "$*"; }
json_event() { printf 'JSON:%s:%s:%s:%s\n' "$1" "$2" "$3" "${4:-}"; }
` + completeFlow + `
complete_installation_flow "/tmp/state" "INSTALL-SUCCESS" "UPGRADE-SUCCESS" "logs"
printf 'unreachable\n'
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) || exitErr.ExitCode() != 18 {
t.Fatalf("rejected credential exit = %v, want 18\n%s", err, out)
}
output := string(out)
if !strings.Contains(output, "JSON:complete:auth_rejected") || !strings.Contains(output, "authentication failed") {
t.Fatalf("rejected credential did not produce actionable non-success completion:\n%s", output)
}
for _, forbidden := range []string{"INSTALL-SUCCESS", "UPGRADE-SUCCESS", "unreachable", "unexpected-proxmox-report"} {
if strings.Contains(output, forbidden) {
t.Fatalf("rejected credential emitted forbidden success output %q:\n%s", forbidden, output)
}
}
}
// TestInstallSHSurfacesBlockedProxmoxRegistration verifies the installer reads
// the agent's proxmox-<type>-registration-blocked marker and prints the denial
// in its own output instead of leaving it buried in the agent journal (#1644).
func TestInstallSHSurfacesBlockedProxmoxRegistration(t *testing.T) {
logInfo := extractInstallShellFunction(t, "log_info")
logWarn := extractInstallShellFunction(t, "log_warn")
logError := extractInstallShellFunction(t, "log_error")
reportFn := extractInstallShellFunction(t, "report_proxmox_registration_outcome")
completeFlow := extractInstallShellFunction(t, "complete_installation_flow")
if !strings.Contains(completeFlow, `report_proxmox_registration_outcome "$state_dir"`) {
t.Fatal("complete_installation_flow does not report the Proxmox registration outcome")
}
clearFn := extractInstallShellFunction(t, "clear_proxmox_state_if_needed")
for _, marker := range []string{"proxmox-pve-registration-blocked", "proxmox-pbs-registration-blocked", "proxmox-detected-types"} {
if !strings.Contains(clearFn, marker) {
t.Fatalf("clear_proxmox_state_if_needed does not clear %s", marker)
}
}
runReport := func(t *testing.T, stateDir string) (string, int) {
t.Helper()
script := `
NON_INTERACTIVE="false"
ENABLE_PROXMOX="true"
redact_token() { printf '%s' "$1"; }
sleep() { :; }
` + logInfo + `
` + logWarn + `
` + logError + `
` + reportFn + `
report_proxmox_registration_outcome "` + stateDir + `"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
rc := 0
if err != nil {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("bash: %v\n%s", err, out)
}
rc = exitErr.ExitCode()
}
return string(out), rc
}
t.Run("blocked marker surfaces reason", func(t *testing.T) {
stateDir := t.TempDir()
reason := "Pulse at https://pulse.local has no PVE source for https://pve.local:8006 and this agent's token cannot create one."
if err := os.WriteFile(filepath.Join(stateDir, "proxmox-pve-registration-blocked"), []byte(reason+"\n"), 0o600); err != nil {
t.Fatalf("write blocked marker: %v", err)
}
out, rc := runReport(t, stateDir)
if rc == 0 {
t.Fatalf("blocked registration reported success:\n%s", out)
}
if !strings.Contains(out, "[ERROR]") || !strings.Contains(out, "registration failed") {
t.Fatalf("blocked registration not surfaced as an error:\n%s", out)
}
if !strings.Contains(out, reason) {
t.Fatalf("blocked registration output missing agent-recorded reason:\n%s", out)
}
})
t.Run("registered marker reports success", func(t *testing.T) {
stateDir := t.TempDir()
if err := os.WriteFile(filepath.Join(stateDir, "proxmox-pve-registered"), []byte("ok\n"), 0o600); err != nil {
t.Fatalf("write registered marker: %v", err)
}
out, rc := runReport(t, stateDir)
if rc != 0 {
t.Fatalf("registered marker returned rc=%d:\n%s", rc, out)
}
if !strings.Contains(out, "Proxmox pve node registered with Pulse.") {
t.Fatalf("registered marker did not report success:\n%s", out)
}
})
// A host with both PVE and PBS installed registers each product separately
// (#1644). The installer must report each outcome rather than letting the
// first marker it finds speak for the whole install.
t.Run("combined host reports both products", func(t *testing.T) {
stateDir := t.TempDir()
writeMarker(t, stateDir, "proxmox-detected-types", "pve\npbs\n")
writeMarker(t, stateDir, "proxmox-pve-registered", "ok\n")
writeMarker(t, stateDir, "proxmox-pbs-registered", "ok\n")
out, rc := runReport(t, stateDir)
if rc != 0 {
t.Fatalf("combined host returned rc=%d:\n%s", rc, out)
}
for _, want := range []string{"Proxmox pve node registered with Pulse.", "Proxmox pbs node registered with Pulse."} {
if !strings.Contains(out, want) {
t.Fatalf("combined host output missing %q:\n%s", want, out)
}
}
if strings.Contains(out, "[ERROR]") {
t.Fatalf("fully registered combined host reported an error:\n%s", out)
}
})
// The regression behind #1644: PVE registered, PBS was refused, and the
// installer printed a bare ERROR banner that read as a failed install.
// Both outcomes must now show up, each attributed to its own product.
t.Run("combined host keeps a successful product visible next to a refusal", func(t *testing.T) {
stateDir := t.TempDir()
reason := "Pulse at https://pulse.local has no PBS source for https://pbs.local:8007 and this agent's token cannot create one."
writeMarker(t, stateDir, "proxmox-detected-types", "pve\npbs\n")
writeMarker(t, stateDir, "proxmox-pve-registered", "ok\n")
writeMarker(t, stateDir, "proxmox-pbs-registration-blocked", reason+"\n")
out, rc := runReport(t, stateDir)
if rc == 0 {
t.Fatalf("refused pbs registration reported overall success:\n%s", out)
}
if !strings.Contains(out, "Proxmox pve node registered with Pulse.") {
t.Fatalf("pve success was erased by the pbs refusal:\n%s", out)
}
if !strings.Contains(out, "Proxmox pbs registration failed:") || !strings.Contains(out, reason) {
t.Fatalf("pbs refusal not surfaced with its reason:\n%s", out)
}
})
// Only the detected products are awaited. A PVE-only host must not sit out
// the full timeout waiting for a PBS outcome that will never arrive.
t.Run("detected types bound what is awaited", func(t *testing.T) {
stateDir := t.TempDir()
writeMarker(t, stateDir, "proxmox-detected-types", "pve\n")
writeMarker(t, stateDir, "proxmox-pve-registered", "ok\n")
out, rc := runReport(t, stateDir)
if rc != 0 {
t.Fatalf("pve-only host returned rc=%d:\n%s", rc, out)
}
if strings.Contains(out, "pbs") {
t.Fatalf("pve-only host reported on pbs:\n%s", out)
}
if strings.Contains(out, "not confirmed") {
t.Fatalf("pve-only host warned about an unconfirmed registration:\n%s", out)
}
})
// Agents older than the detected-types marker keep the previous timing:
// the first outcome that appears ends the wait.
t.Run("missing detected types falls back to first outcome", func(t *testing.T) {
stateDir := t.TempDir()
writeMarker(t, stateDir, "proxmox-pbs-registered", "ok\n")
out, rc := runReport(t, stateDir)
if rc != 0 {
t.Fatalf("legacy agent marker returned rc=%d:\n%s", rc, out)
}
if !strings.Contains(out, "Proxmox pbs node registered with Pulse.") {
t.Fatalf("legacy agent marker did not report success:\n%s", out)
}
if strings.Contains(out, "not confirmed") {
t.Fatalf("legacy agent marker warned about an unconfirmed registration:\n%s", out)
}
})
t.Run("no outcome warns once", func(t *testing.T) {
stateDir := t.TempDir()
out, rc := runReport(t, stateDir)
if rc != 0 {
t.Fatalf("unconfirmed registration returned rc=%d:\n%s", rc, out)
}
if !strings.Contains(out, "not confirmed") || !strings.Contains(out, "Check the agent logs") {
t.Fatalf("unconfirmed registration did not warn:\n%s", out)
}
})
}
func writeMarker(t *testing.T, stateDir, name, contents string) {
t.Helper()
if err := os.WriteFile(filepath.Join(stateDir, name), []byte(contents), 0o600); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
// 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)
}
}
}
// TestInstallSHAgentKillPatternsExcludeSiblingAgents guards a real incident:
// the installer and the Unraid wrapper it generates both killed agents with
// pkill -f "^<binary>". pkill -f matches the whole command line and "^" only
// anchors the start, so on a host running a second, co-installed agent whose
// binary name merely starts with the same prefix (pulse-agent alongside
// pulse-agent-prod) every install, upgrade, and wrapper restart silently took
// down the other agent too.
func TestInstallSHAgentKillPatternsExcludeSiblingAgents(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
// Every pkill -f whose pattern is anchored at the agent binary must also
// bound the far end, or it matches the sibling agent.
unbounded := regexp.MustCompile(`pkill[^\n]*-f "\^\$\{(?:RUNTIME_BINARY|INSTALL_DIR)\}[^"\n]*"`)
for _, match := range unbounded.FindAllString(string(content), -1) {
if !strings.Contains(match, "[[:space:]]") {
t.Errorf("unbounded agent pkill pattern would also match a sibling agent: %s", match)
}
}
// A bare substring kill is worse still: it needs no prefix anchor at all.
if strings.Contains(string(content), `pkill -9 -f "pulse-agent"`) {
t.Error(`pkill -9 -f "pulse-agent" matches pulse-agent-prod; use -x on the exact process name`)
}
}
// TestInstallSHAgentKillPatternSparesSiblingAgent proves the pattern semantics
// rather than its spelling. pkill -f applies a POSIX ERE to the whole command
// line, so the same engine is exercised here against the two command lines a
// dual-agent host actually presents.
func TestInstallSHAgentKillPatternSparesSiblingAgent(t *testing.T) {
const (
targetCmd = "/usr/local/bin/pulse-agent --url http://192.168.0.113:7655 --interval 30s"
siblingCmd = "/usr/local/bin/pulse-agent-prod --url http://192.168.0.220:7655 --interval 30s"
)
ereMatches := func(t *testing.T, pattern, line string) bool {
t.Helper()
cmd := exec.Command("grep", "-E", "-q", pattern)
cmd.Stdin = strings.NewReader(line + "\n")
err := cmd.Run()
if err == nil {
return true
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
return false
}
t.Fatalf("grep -E %q: %v", pattern, err)
return false
}
unbounded := "^/usr/local/bin/pulse-agent"
if !ereMatches(t, unbounded, siblingCmd) {
t.Fatal("premise check failed: the unbounded pattern is supposed to match the sibling agent")
}
bounded := "^/usr/local/bin/pulse-agent([[:space:]]|$)"
if !ereMatches(t, bounded, targetCmd) {
t.Error("bounded pattern must still match its own agent")
}
if ereMatches(t, bounded, siblingCmd) {
t.Error("bounded pattern must not match pulse-agent-prod")
}
}
// TestInstallSHUnraidStopsWatchdogBeforeAgent guards a real incident: the
// Unraid install path killed the agent but never the wrapper supervising it,
// then started a second wrapper at the end of the install. The survivor and
// the newcomer both loop trying to own the same agent id, and because the old
// wrapper is a watchdog it can respawn the agent during the install window
// using the previous binary and arguments.
func TestInstallSHUnraidStopsWatchdogBeforeAgent(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
start := strings.Index(script, "if [[ -f /etc/unraid-version ]]; then")
if start < 0 {
t.Fatal("could not locate the Unraid install branch")
}
endOffset := strings.Index(script[start:], "\n# 4.")
if endOffset < 0 {
endOffset = len(script) - start
}
unraid := script[start : start+endOffset]
wrapperKill := strings.Index(unraid, `pkill -f "/start-pulse-agent\.sh`)
if wrapperKill < 0 {
t.Fatal("Unraid install must stop the existing wrapper supervisor, or its watchdog survives the install and races the new one")
}
agentKill := strings.Index(unraid, `pkill -f "^${RUNTIME_BINARY}`)
if agentKill < 0 {
t.Fatal("Unraid install must stop the existing agent")
}
if wrapperKill > agentKill {
t.Error("the wrapper supervisor must be stopped before the agent, otherwise the watchdog respawns the agent during the install")
}
}
// TestInstallSHWatchdogKillPatternSparesSiblingWrapper proves the wrapper
// pattern discriminates, so stopping this agent's supervisor never stops a
// co-installed agent's supervisor on the same host.
func TestInstallSHWatchdogKillPatternSparesSiblingWrapper(t *testing.T) {
const (
targetCmd = "bash /boot/config/plugins/pulse-agent/start-pulse-agent.sh"
siblingCmd = "bash /boot/config/plugins/pulse-agent-prod/start-pulse-agent-prod.sh"
)
ereMatches := func(t *testing.T, pattern, line string) bool {
t.Helper()
cmd := exec.Command("grep", "-E", "-q", pattern)
cmd.Stdin = strings.NewReader(line + "\n")
err := cmd.Run()
if err == nil {
return true
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
return false
}
t.Fatalf("grep -E %q: %v", pattern, err)
return false
}
pattern := `/start-pulse-agent\.sh([[:space:]]|$)`
if !ereMatches(t, pattern, targetCmd) {
t.Error("wrapper pattern must match this agent's own supervisor")
}
if ereMatches(t, pattern, siblingCmd) {
t.Error("wrapper pattern must not match a co-installed agent's supervisor")
}
// Premise check: an unescaped dot is what makes this worth pinning.
if !ereMatches(t, "start-pulse-agent.sh", targetCmd) {
t.Fatal("premise check failed: the loose pattern is supposed to match the target")
}
}
// TestInstallSHWrapperKillsAreBoundedEverywhere pins every wrapper kill in the
// installer, not just the Unraid install branch. A bare "start-pulse-agent.sh"
// pattern leaves the dot as a wildcard and the far end unbounded, so it also
// matches a .bak copy of the wrapper or an editor session holding it open.
func TestInstallSHWrapperKillsAreBoundedEverywhere(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
if strings.Contains(script, `pkill -f "start-pulse-agent.sh"`) {
t.Error(`bare pkill -f "start-pulse-agent.sh" leaves the dot as a wildcard and the tail unbounded`)
}
wrapperKill := regexp.MustCompile(`pkill -f "[^"\n]*start-pulse-agent[^"\n]*"`)
found := wrapperKill.FindAllString(script, -1)
if len(found) == 0 {
t.Fatal("expected the installer to stop wrapper supervisors somewhere")
}
for _, match := range found {
if !strings.Contains(match, `\.sh`) {
t.Errorf("wrapper kill must escape the dot: %s", match)
}
if !strings.Contains(match, "[[:space:]]") {
t.Errorf("wrapper kill must bound its far end: %s", match)
}
}
}
// TestInstallSHStopsWrapperBeforeAgentInEveryBranch pins the ordering rule the
// agent-lifecycle and deployment-installability contracts now carry: a wrapper
// is a watchdog, so stopping the agent while its wrapper still loops merely
// races the respawn. Every branch that stops both must stop the wrapper first.
func TestInstallSHStopsWrapperBeforeAgentInEveryBranch(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
lines := strings.Split(string(content), "\n")
// Walk the file and, for each wrapper kill, confirm the nearest agent kill
// that shares its stop block does not precede it.
for i, line := range lines {
if !strings.Contains(line, "pkill") || !strings.Contains(line, "start-pulse-agent") {
continue
}
// Look back a short window for an agent kill in the same block.
for back := i - 1; back >= 0 && back > i-12; back-- {
prev := lines[back]
if !strings.Contains(prev, "pkill") {
continue
}
if strings.Contains(prev, "start-pulse-agent") {
break
}
if strings.Contains(prev, `-x "pulse-agent"`) || strings.Contains(prev, "${RUNTIME_BINARY}") ||
strings.Contains(prev, "${BINARY_NAME}") || strings.Contains(prev, "${INSTALL_DIR}") {
t.Errorf("line %d stops the agent before its wrapper at line %d; the watchdog will respawn it:\n %s\n %s",
back+1, i+1, strings.TrimSpace(prev), strings.TrimSpace(line))
}
break
}
}
}
// TestInstallSHVersionMismatchWarningIgnoresBuildMetadata pins the comparison
// behind the "Downloaded agent version does not match" warning. A server built
// from a working tree reports build metadata the agent it serves never carries,
// so a raw comparison fired on every correct development install. That is not a
// cosmetic annoyance: the warning is the only client-side signal that a stale
// agent was downloaded, and one that cries wolf gets skipped the time it counts.
func TestInstallSHVersionMismatchWarningIgnoresBuildMetadata(t *testing.T) {
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
if err != nil {
t.Fatalf("read install.sh: %v", err)
}
script := string(content)
for _, required := range []string{
`NEW_VERSION_NORMALIZED="${NEW_VERSION_NORMALIZED%%+*}"`,
`SERVER_VERSION_NORMALIZED="${SERVER_VERSION_NORMALIZED%%+*}"`,
} {
if !strings.Contains(script, required) {
t.Errorf("version comparison must strip semver build metadata, missing: %s", required)
}
}
// Exercise the same normalisation the installer performs.
normalize := func(v string) string {
v = strings.TrimPrefix(v, "v")
if idx := strings.Index(v, "+"); idx >= 0 {
v = v[:idx]
}
return v
}
cases := []struct {
agent string
server string
warns bool
}{
// The shape that fired on every correct dev install.
{"v6.2.0-rc.8", "6.2.0-rc.8+git.46.g98a638e00.dirty", false},
{"v6.2.0-rc.8", "6.2.0-rc.8", false},
// The stale download this warning exists to catch.
{"v6.0.5-54-gc862fb0ca0", "6.2.0-rc.8+git.46.g98a638e00.dirty", true},
// A prerelease is genuinely not its release.
{"v6.2.0", "6.2.0-rc.8+git.46.gabc.dirty", true},
{"v6.1.2", "6.2.0-rc.8", true},
}
for _, tc := range cases {
got := normalize(tc.agent) != normalize(tc.server)
if got != tc.warns {
t.Errorf("agent %q vs server %q: warns=%v, want %v", tc.agent, tc.server, got, tc.warns)
}
}
}