aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/test_linux/block-clone-test.sh (renamed from scripts/test_scripts/block-clone-test.sh)2
-rwxr-xr-xscripts/test_linux/service-test.sh380
-rwxr-xr-xscripts/test_mac/block-clone-test.sh (renamed from scripts/test_scripts/block-clone-test-mac.sh)2
-rwxr-xr-xscripts/test_mac/service-test.sh368
-rw-r--r--scripts/test_windows/block-clone-test.ps1 (renamed from scripts/test_scripts/block-clone-test-windows.ps1)2
-rw-r--r--scripts/test_windows/service-test.ps1353
6 files changed, 1104 insertions, 3 deletions
diff --git a/scripts/test_scripts/block-clone-test.sh b/scripts/test_linux/block-clone-test.sh
index 7c6bf5605..0a74283f2 100755
--- a/scripts/test_scripts/block-clone-test.sh
+++ b/scripts/test_linux/block-clone-test.sh
@@ -4,7 +4,7 @@
# Requires: root/sudo, btrfs-progs (mkfs.btrfs), xfsprogs (mkfs.xfs)
#
# Usage:
-# sudo ./scripts/test_scripts/block-clone-test.sh [path-to-zencore-test]
+# sudo ./scripts/test_linux/block-clone-test.sh [path-to-zencore-test]
#
# If no path is given, defaults to build/linux/x86_64/debug/zencore-test
# relative to the repository root.
diff --git a/scripts/test_linux/service-test.sh b/scripts/test_linux/service-test.sh
new file mode 100755
index 000000000..f91339e1b
--- /dev/null
+++ b/scripts/test_linux/service-test.sh
@@ -0,0 +1,380 @@
+#!/usr/bin/env bash
+# Test zen service command lifecycle on Linux (systemd).
+#
+# Requires: root/sudo, systemd
+#
+# Usage:
+# sudo ./scripts/test_linux/service-test.sh [path-to-zen]
+#
+# If no path is given, defaults to build/linux/x86_64/debug/zen
+# relative to the repository root.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+ZEN_BINARY="${1:-$REPO_ROOT/build/linux/x86_64/debug/zen}"
+ZENSERVER_BINARY="$(dirname "$ZEN_BINARY")/zenserver"
+SERVICE_NAME="ZenServerTest-$$"
+UNIT_NAME="com.epicgames.unreal.${SERVICE_NAME}"
+UNIT_FILE="/etc/systemd/system/${UNIT_NAME}.service"
+
+PASSED=0
+FAILED=0
+TESTS_RUN=0
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+cleanup() {
+ local exit_code=$?
+ set +e
+
+ echo ""
+ echo "--- Cleanup ---"
+
+ # Stop service if running
+ if systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
+ echo "Stopping test service..."
+ systemctl stop "$UNIT_NAME" 2>/dev/null
+ fi
+
+ # Disable and remove unit file
+ if [ -f "$UNIT_FILE" ]; then
+ echo "Removing test unit file..."
+ systemctl disable "$UNIT_NAME" 2>/dev/null || true
+ rm -f "$UNIT_FILE"
+ systemctl daemon-reload 2>/dev/null || true
+ fi
+
+ echo ""
+ echo "=============================="
+ printf " Tests run: %d\n" "$TESTS_RUN"
+ printf " ${GREEN}Passed: %d${NC}\n" "$PASSED"
+ if [ "$FAILED" -gt 0 ]; then
+ printf " ${RED}Failed: %d${NC}\n" "$FAILED"
+ else
+ printf " Failed: %d\n" "$FAILED"
+ fi
+ echo "=============================="
+
+ if [ "$FAILED" -gt 0 ]; then
+ exit 1
+ fi
+ exit "$exit_code"
+}
+
+trap cleanup EXIT
+
+pass() {
+ TESTS_RUN=$((TESTS_RUN + 1))
+ PASSED=$((PASSED + 1))
+ printf " ${GREEN}PASS${NC}: %s\n" "$1"
+}
+
+fail() {
+ TESTS_RUN=$((TESTS_RUN + 1))
+ FAILED=$((FAILED + 1))
+ printf " ${RED}FAIL${NC}: %s\n" "$1"
+ if [ -n "${2:-}" ]; then
+ printf " %s\n" "$2"
+ fi
+}
+
+# ── Preflight checks ──────────────────────────────────────────────
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "Error: this test must be run as root (sudo)."
+ exit 1
+fi
+
+if ! command -v systemctl &>/dev/null; then
+ echo "Error: systemctl not found — this test requires systemd."
+ exit 1
+fi
+
+if [ ! -x "$ZEN_BINARY" ]; then
+ echo "Error: zen binary not found at '$ZEN_BINARY'"
+ echo "Build with: xmake config -m debug && xmake build zen"
+ exit 1
+fi
+
+if [ ! -x "$ZENSERVER_BINARY" ]; then
+ echo "Error: zenserver binary not found at '$ZENSERVER_BINARY'"
+ echo "Build with: xmake config -m debug && xmake build zenserver"
+ exit 1
+fi
+
+echo "zen binary: $ZEN_BINARY"
+echo "zenserver binary: $ZENSERVER_BINARY"
+echo "service name: $SERVICE_NAME"
+echo "unit name: $UNIT_NAME"
+echo ""
+
+# Determine which user to run the service as (the user who invoked sudo)
+SERVICE_USER="${SUDO_USER:-$(whoami)}"
+
+# ── Test: status before install (should fail) ─────────────────────
+
+echo "--- Test: status before install ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "status reports 'not installed' for non-existent service"
+else
+ fail "status should report 'not installed'" "got: $OUTPUT"
+fi
+
+# ── Test: install ─────────────────────────────────────────────────
+
+echo "--- Test: install ---"
+
+OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" --user "$SERVICE_USER" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "install exits with code 0"
+else
+ fail "install exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+if [ -f "$UNIT_FILE" ]; then
+ pass "unit file created at $UNIT_FILE"
+else
+ fail "unit file created at $UNIT_FILE"
+fi
+
+# Verify unit file contents
+if [ -f "$UNIT_FILE" ]; then
+ if grep -q "ExecStart=.*zenserver" "$UNIT_FILE"; then
+ pass "unit file contains ExecStart with zenserver"
+ else
+ fail "unit file contains ExecStart with zenserver" "$(cat "$UNIT_FILE")"
+ fi
+
+ if grep -q "User=$SERVICE_USER" "$UNIT_FILE"; then
+ pass "unit file contains correct User=$SERVICE_USER"
+ else
+ fail "unit file contains correct User=$SERVICE_USER" "$(grep User "$UNIT_FILE")"
+ fi
+
+ if grep -q "Type=notify" "$UNIT_FILE"; then
+ pass "unit file uses Type=notify"
+ else
+ fail "unit file uses Type=notify"
+ fi
+
+ if grep -q "WantedBy=multi-user.target" "$UNIT_FILE"; then
+ pass "unit file has WantedBy=multi-user.target"
+ else
+ fail "unit file has WantedBy=multi-user.target"
+ fi
+
+ # Verify the service is enabled
+ if systemctl is-enabled --quiet "$UNIT_NAME" 2>/dev/null; then
+ pass "service is enabled after install"
+ else
+ fail "service is enabled after install"
+ fi
+fi
+
+# ── Test: install again (already installed, no --full) ────────────
+
+echo "--- Test: install again (idempotent) ---"
+
+OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" --user "$SERVICE_USER" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "re-install exits with code 0"
+else
+ fail "re-install exits with code 0" "got exit code: $EXIT_CODE"
+fi
+
+if echo "$OUTPUT" | grep -qi "already installed"; then
+ pass "re-install reports service already installed"
+else
+ fail "re-install reports service already installed" "got: $OUTPUT"
+fi
+
+# ── Test: status after install (not yet started) ──────────────────
+
+echo "--- Test: status after install (stopped) ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+# The status command throws if not running, so we expect an error message
+if echo "$OUTPUT" | grep -qi "not running"; then
+ pass "status reports 'not running' for stopped service"
+else
+ fail "status reports 'not running' for stopped service" "got: $OUTPUT"
+fi
+
+# ── Test: start ───────────────────────────────────────────────────
+
+echo "--- Test: start ---"
+
+OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "start exits with code 0"
+else
+ # The TODO comments in the code indicate start may not work perfectly
+ fail "start exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+# Give the service a moment to start
+sleep 1
+
+if systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
+ pass "service is active after start"
+
+ # ── Test: status while running ────────────────────────────────
+
+ echo "--- Test: status (running) ---"
+
+ OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+ if echo "$OUTPUT" | grep -qi "Running"; then
+ pass "status reports 'Running'"
+ else
+ fail "status reports 'Running'" "got: $OUTPUT"
+ fi
+
+ if echo "$OUTPUT" | grep -qi "zenserver"; then
+ pass "status shows executable path"
+ else
+ fail "status shows executable path" "got: $OUTPUT"
+ fi
+
+ # ── Test: start again (already running) ───────────────────────
+
+ echo "--- Test: start again (already running) ---"
+
+ OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1)
+ if echo "$OUTPUT" | grep -qi "already running"; then
+ pass "start reports service already running"
+ else
+ fail "start reports service already running" "got: $OUTPUT"
+ fi
+
+ # ── Test: stop ────────────────────────────────────────────────
+
+ echo "--- Test: stop ---"
+
+ OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1)
+ EXIT_CODE=$?
+
+ if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "stop exits with code 0"
+ else
+ fail "stop exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+ fi
+
+ sleep 1
+
+ if ! systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
+ pass "service is inactive after stop"
+ else
+ fail "service is inactive after stop"
+ fi
+else
+ fail "service is active after start" "(skipping start-dependent tests)"
+fi
+
+# ── Test: stop when already stopped ───────────────────────────────
+
+echo "--- Test: stop when already stopped ---"
+
+# Make sure it's stopped first
+systemctl stop "$UNIT_NAME" 2>/dev/null || true
+sleep 1
+
+OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not running"; then
+ pass "stop reports 'not running' when already stopped"
+else
+ fail "stop reports 'not running' when already stopped" "got: $OUTPUT"
+fi
+
+# ── Test: uninstall while running (should fail) ───────────────────
+
+echo "--- Test: uninstall while running (should fail) ---"
+
+# Start the service so we can test uninstall-while-running
+"$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1 || true
+sleep 1
+
+if systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
+ OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1 || true)
+ EXIT_CODE=$?
+
+ if [ "$EXIT_CODE" -ne 0 ] || echo "$OUTPUT" | grep -qi "running.*stop"; then
+ pass "uninstall refuses while service is running"
+ else
+ fail "uninstall refuses while service is running" "got: exit=$EXIT_CODE, output: $OUTPUT"
+ fi
+
+ # Stop it for the real uninstall test
+ "$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true
+ systemctl stop "$UNIT_NAME" 2>/dev/null || true
+ sleep 1
+else
+ echo " (skipped: could not start service for this test)"
+fi
+
+# ── Test: uninstall ───────────────────────────────────────────────
+
+echo "--- Test: uninstall ---"
+
+# Ensure stopped
+systemctl stop "$UNIT_NAME" 2>/dev/null || true
+sleep 1
+
+OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "uninstall exits with code 0"
+else
+ fail "uninstall exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+if [ ! -f "$UNIT_FILE" ]; then
+ pass "unit file removed after uninstall"
+else
+ fail "unit file removed after uninstall"
+fi
+
+# ── Test: status after uninstall ──────────────────────────────────
+
+echo "--- Test: status after uninstall ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "status reports 'not installed' after uninstall"
+else
+ fail "status reports 'not installed' after uninstall" "got: $OUTPUT"
+fi
+
+# ── Test: uninstall when not installed (idempotent) ───────────────
+
+echo "--- Test: uninstall when not installed ---"
+
+OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "uninstall of non-existent service exits with code 0"
+else
+ fail "uninstall of non-existent service exits with code 0" "got exit code: $EXIT_CODE"
+fi
+
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "uninstall reports service not installed"
+else
+ fail "uninstall reports service not installed" "got: $OUTPUT"
+fi
diff --git a/scripts/test_scripts/block-clone-test-mac.sh b/scripts/test_mac/block-clone-test.sh
index a3d3ca4d3..1a9575dcf 100755
--- a/scripts/test_scripts/block-clone-test-mac.sh
+++ b/scripts/test_mac/block-clone-test.sh
@@ -5,7 +5,7 @@
# clonefile(), so no special setup is needed — just run the tests.
#
# Usage:
-# ./scripts/test_scripts/block-clone-test-mac.sh [path-to-zencore-test]
+# ./scripts/test_mac/block-clone-test.sh [path-to-zencore-test]
#
# If no path is given, defaults to build/macosx/<arch>/debug/zencore-test
# relative to the repository root.
diff --git a/scripts/test_mac/service-test.sh b/scripts/test_mac/service-test.sh
new file mode 100755
index 000000000..da4a966e7
--- /dev/null
+++ b/scripts/test_mac/service-test.sh
@@ -0,0 +1,368 @@
+#!/usr/bin/env bash
+# Test zen service command lifecycle on macOS (launchd).
+#
+# Does NOT require sudo — macOS service commands run as the current user
+# via ~/Library/LaunchAgents.
+#
+# Usage:
+# ./scripts/test_mac/service-test.sh [path-to-zen]
+#
+# If no path is given, defaults to build/macosx/<arch>/debug/zen
+# relative to the repository root.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+ARCH="$(uname -m)"
+ZEN_BINARY="${1:-$REPO_ROOT/build/macosx/$ARCH/debug/zen}"
+ZENSERVER_BINARY="$(dirname "$ZEN_BINARY")/zenserver"
+SERVICE_NAME="ZenServerTest-$$"
+DAEMON_NAME="com.epicgames.unreal.${SERVICE_NAME}"
+PLIST_FILE="$HOME/Library/LaunchAgents/${DAEMON_NAME}.plist"
+
+PASSED=0
+FAILED=0
+TESTS_RUN=0
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m'
+
+cleanup() {
+ local exit_code=$?
+ set +e
+
+ echo ""
+ echo "--- Cleanup ---"
+
+ # Bootout (stop) the agent if loaded
+ if launchctl list "$DAEMON_NAME" &>/dev/null; then
+ echo "Stopping test service..."
+ launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true
+ fi
+
+ # Remove plist file
+ if [ -f "$PLIST_FILE" ]; then
+ echo "Removing test plist file..."
+ rm -f "$PLIST_FILE"
+ fi
+
+ echo ""
+ echo "=============================="
+ printf " Tests run: %d\n" "$TESTS_RUN"
+ printf " ${GREEN}Passed: %d${NC}\n" "$PASSED"
+ if [ "$FAILED" -gt 0 ]; then
+ printf " ${RED}Failed: %d${NC}\n" "$FAILED"
+ else
+ printf " Failed: %d\n" "$FAILED"
+ fi
+ echo "=============================="
+
+ if [ "$FAILED" -gt 0 ]; then
+ exit 1
+ fi
+ exit "$exit_code"
+}
+
+trap cleanup EXIT
+
+pass() {
+ TESTS_RUN=$((TESTS_RUN + 1))
+ PASSED=$((PASSED + 1))
+ printf " ${GREEN}PASS${NC}: %s\n" "$1"
+}
+
+fail() {
+ TESTS_RUN=$((TESTS_RUN + 1))
+ FAILED=$((FAILED + 1))
+ printf " ${RED}FAIL${NC}: %s\n" "$1"
+ if [ -n "${2:-}" ]; then
+ printf " %s\n" "$2"
+ fi
+}
+
+# ── Preflight checks ──────────────────────────────────────────────
+
+if [ "$(uname)" != "Darwin" ]; then
+ echo "Error: this test is for macOS only."
+ exit 1
+fi
+
+if ! command -v launchctl &>/dev/null; then
+ echo "Error: launchctl not found."
+ exit 1
+fi
+
+if [ ! -x "$ZEN_BINARY" ]; then
+ echo "Error: zen binary not found at '$ZEN_BINARY'"
+ echo "Build with: xmake config -m debug && xmake build zen"
+ exit 1
+fi
+
+if [ ! -x "$ZENSERVER_BINARY" ]; then
+ echo "Error: zenserver binary not found at '$ZENSERVER_BINARY'"
+ echo "Build with: xmake config -m debug && xmake build zenserver"
+ exit 1
+fi
+
+echo "zen binary: $ZEN_BINARY"
+echo "zenserver binary: $ZENSERVER_BINARY"
+echo "service name: $SERVICE_NAME"
+echo "daemon name: $DAEMON_NAME"
+echo "plist path: $PLIST_FILE"
+echo ""
+
+# ── Test: status before install (should fail) ─────────────────────
+
+echo "--- Test: status before install ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "status reports 'not installed' for non-existent service"
+else
+ fail "status should report 'not installed'" "got: $OUTPUT"
+fi
+
+# ── Test: install ─────────────────────────────────────────────────
+
+echo "--- Test: install ---"
+
+OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "install exits with code 0"
+else
+ fail "install exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+if [ -f "$PLIST_FILE" ]; then
+ pass "plist file created at $PLIST_FILE"
+else
+ fail "plist file created at $PLIST_FILE"
+fi
+
+# Verify plist contents
+if [ -f "$PLIST_FILE" ]; then
+ if grep -q "<string>$ZENSERVER_BINARY</string>" "$PLIST_FILE"; then
+ pass "plist contains zenserver executable path"
+ else
+ fail "plist contains zenserver executable path" "$(cat "$PLIST_FILE")"
+ fi
+
+ if grep -q "<key>Label</key>" "$PLIST_FILE"; then
+ pass "plist has Label key"
+ else
+ fail "plist has Label key"
+ fi
+
+ if grep -q "$DAEMON_NAME" "$PLIST_FILE"; then
+ pass "plist contains correct daemon name"
+ else
+ fail "plist contains correct daemon name"
+ fi
+
+ if grep -q "<key>ProgramArguments</key>" "$PLIST_FILE"; then
+ pass "plist has ProgramArguments"
+ else
+ fail "plist has ProgramArguments"
+ fi
+fi
+
+# ── Test: install again (already installed, no --full) ────────────
+
+echo "--- Test: install again (idempotent) ---"
+
+OUTPUT=$("$ZEN_BINARY" service install "$ZENSERVER_BINARY" "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "re-install exits with code 0"
+else
+ fail "re-install exits with code 0" "got exit code: $EXIT_CODE"
+fi
+
+if echo "$OUTPUT" | grep -qi "already installed"; then
+ pass "re-install reports service already installed"
+else
+ fail "re-install reports service already installed" "got: $OUTPUT"
+fi
+
+# ── Test: status after install (not yet started) ──────────────────
+
+echo "--- Test: status after install (stopped) ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not running"; then
+ pass "status reports 'not running' for stopped service"
+else
+ fail "status reports 'not running' for stopped service" "got: $OUTPUT"
+fi
+
+# ── Test: start ───────────────────────────────────────────────────
+
+echo "--- Test: start ---"
+
+OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "start exits with code 0"
+else
+ fail "start exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+# Give the service a moment to start
+sleep 2
+
+if launchctl list "$DAEMON_NAME" &>/dev/null; then
+ pass "service is loaded after start"
+
+ # ── Test: status while running ────────────────────────────────
+
+ echo "--- Test: status (running) ---"
+
+ OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+ if echo "$OUTPUT" | grep -qi "Running"; then
+ pass "status reports 'Running'"
+ else
+ fail "status reports 'Running'" "got: $OUTPUT"
+ fi
+
+ if echo "$OUTPUT" | grep -qi "zenserver"; then
+ pass "status shows executable path"
+ else
+ fail "status shows executable path" "got: $OUTPUT"
+ fi
+
+ # ── Test: start again (already running) ───────────────────────
+
+ echo "--- Test: start again (already running) ---"
+
+ OUTPUT=$("$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1)
+ if echo "$OUTPUT" | grep -qi "already running"; then
+ pass "start reports service already running"
+ else
+ fail "start reports service already running" "got: $OUTPUT"
+ fi
+
+ # ── Test: stop ────────────────────────────────────────────────
+
+ echo "--- Test: stop ---"
+
+ OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1)
+ EXIT_CODE=$?
+
+ if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "stop exits with code 0"
+ else
+ fail "stop exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+ fi
+
+ sleep 1
+
+ if ! launchctl list "$DAEMON_NAME" &>/dev/null; then
+ pass "service is unloaded after stop"
+ else
+ fail "service is unloaded after stop"
+ fi
+else
+ fail "service is loaded after start" "(skipping start-dependent tests)"
+fi
+
+# ── Test: stop when already stopped ───────────────────────────────
+
+echo "--- Test: stop when already stopped ---"
+
+# Make sure it's stopped first
+launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true
+sleep 1
+
+OUTPUT=$("$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not running"; then
+ pass "stop reports 'not running' when already stopped"
+else
+ fail "stop reports 'not running' when already stopped" "got: $OUTPUT"
+fi
+
+# ── Test: uninstall while running (should fail) ───────────────────
+
+echo "--- Test: uninstall while running (should fail) ---"
+
+# Start the service so we can test uninstall-while-running
+"$ZEN_BINARY" service start "$SERVICE_NAME" 2>&1 || true
+sleep 2
+
+if launchctl list "$DAEMON_NAME" &>/dev/null; then
+ OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1 || true)
+ EXIT_CODE=$?
+
+ if [ "$EXIT_CODE" -ne 0 ] || echo "$OUTPUT" | grep -qi "running.*stop"; then
+ pass "uninstall refuses while service is running"
+ else
+ fail "uninstall refuses while service is running" "got: exit=$EXIT_CODE, output: $OUTPUT"
+ fi
+
+ # Stop it for the real uninstall test
+ "$ZEN_BINARY" service stop "$SERVICE_NAME" 2>&1 || true
+ launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true
+ sleep 1
+else
+ echo " (skipped: could not start service for this test)"
+fi
+
+# ── Test: uninstall ───────────────────────────────────────────────
+
+echo "--- Test: uninstall ---"
+
+# Ensure stopped
+launchctl bootout "gui/$(id -u)" "$PLIST_FILE" 2>/dev/null || true
+sleep 1
+
+OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "uninstall exits with code 0"
+else
+ fail "uninstall exits with code 0" "got exit code: $EXIT_CODE, output: $OUTPUT"
+fi
+
+if [ ! -f "$PLIST_FILE" ]; then
+ pass "plist file removed after uninstall"
+else
+ fail "plist file removed after uninstall"
+fi
+
+# ── Test: status after uninstall ──────────────────────────────────
+
+echo "--- Test: status after uninstall ---"
+
+OUTPUT=$("$ZEN_BINARY" service status "$SERVICE_NAME" 2>&1 || true)
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "status reports 'not installed' after uninstall"
+else
+ fail "status reports 'not installed' after uninstall" "got: $OUTPUT"
+fi
+
+# ── Test: uninstall when not installed (idempotent) ───────────────
+
+echo "--- Test: uninstall when not installed ---"
+
+OUTPUT=$("$ZEN_BINARY" service uninstall "$SERVICE_NAME" 2>&1)
+EXIT_CODE=$?
+
+if [ "$EXIT_CODE" -eq 0 ]; then
+ pass "uninstall of non-existent service exits with code 0"
+else
+ fail "uninstall of non-existent service exits with code 0" "got exit code: $EXIT_CODE"
+fi
+
+if echo "$OUTPUT" | grep -qi "not installed"; then
+ pass "uninstall reports service not installed"
+else
+ fail "uninstall reports service not installed" "got: $OUTPUT"
+fi
diff --git a/scripts/test_scripts/block-clone-test-windows.ps1 b/scripts/test_windows/block-clone-test.ps1
index df24831a4..aa6ec3a39 100644
--- a/scripts/test_scripts/block-clone-test-windows.ps1
+++ b/scripts/test_windows/block-clone-test.ps1
@@ -7,7 +7,7 @@
#
# Usage:
# # From an elevated PowerShell prompt:
-# .\scripts\test_scripts\block-clone-test-windows.ps1 [-TestBinary <path>]
+# .\scripts\test_windows\block-clone-test.ps1 [-TestBinary <path>]
#
# If -TestBinary is not given, defaults to build\windows\x64\debug\zencore-test.exe
# relative to the repository root.
diff --git a/scripts/test_windows/service-test.ps1 b/scripts/test_windows/service-test.ps1
new file mode 100644
index 000000000..4c484c63f
--- /dev/null
+++ b/scripts/test_windows/service-test.ps1
@@ -0,0 +1,353 @@
+# Test zen service command lifecycle on Windows (SCM).
+#
+# Requires: Administrator privileges
+#
+# Usage:
+# # From an elevated PowerShell prompt:
+# .\scripts\test_windows\service-test.ps1 [-ZenBinary <path>]
+#
+# If -ZenBinary is not given, defaults to build\windows\x64\debug\zen.exe
+# relative to the repository root.
+
+param(
+ [string]$ZenBinary
+)
+
+$ErrorActionPreference = "Stop"
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$RepoRoot = (Resolve-Path "$ScriptDir\..\..").Path
+
+if (-not $ZenBinary) {
+ $ZenBinary = Join-Path $RepoRoot "build\windows\x64\debug\zen.exe"
+}
+$ZenServerBinary = Join-Path (Split-Path -Parent $ZenBinary) "zenserver.exe"
+$ServiceName = "ZenServerTest-$PID"
+
+$Script:Passed = 0
+$Script:Failed = 0
+$Script:TestsRun = 0
+
+function Pass($Message) {
+ $Script:TestsRun++
+ $Script:Passed++
+ Write-Host " PASS: $Message" -ForegroundColor Green
+}
+
+function Fail($Message, $Detail) {
+ $Script:TestsRun++
+ $Script:Failed++
+ Write-Host " FAIL: $Message" -ForegroundColor Red
+ if ($Detail) {
+ Write-Host " $Detail"
+ }
+}
+
+function Cleanup {
+ Write-Host ""
+ Write-Host "--- Cleanup ---"
+
+ # Stop the service if running
+ $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+ if ($svc -and $svc.Status -eq "Running") {
+ Write-Host "Stopping test service..."
+ Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 2
+ }
+
+ # Delete the service if it exists
+ if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) {
+ Write-Host "Removing test service..."
+ sc.exe delete $ServiceName 2>$null | Out-Null
+ }
+
+ Write-Host ""
+ Write-Host "=============================="
+ Write-Host " Tests run: $Script:TestsRun"
+ Write-Host " Passed: $Script:Passed" -ForegroundColor Green
+ if ($Script:Failed -gt 0) {
+ Write-Host " Failed: $Script:Failed" -ForegroundColor Red
+ } else {
+ Write-Host " Failed: $Script:Failed"
+ }
+ Write-Host "=============================="
+}
+
+# ── Preflight checks ──────────────────────────────────────────────
+
+$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
+ [Security.Principal.WindowsBuiltInRole]::Administrator)
+
+if (-not $IsAdmin) {
+ Write-Host "Error: this test must be run from an elevated (Administrator) prompt." -ForegroundColor Red
+ exit 1
+}
+
+if (-not (Test-Path $ZenBinary)) {
+ Write-Host "Error: zen binary not found at '$ZenBinary'" -ForegroundColor Red
+ Write-Host "Build with: xmake config -m debug && xmake build zen"
+ exit 1
+}
+
+if (-not (Test-Path $ZenServerBinary)) {
+ Write-Host "Error: zenserver binary not found at '$ZenServerBinary'" -ForegroundColor Red
+ Write-Host "Build with: xmake config -m debug && xmake build zenserver"
+ exit 1
+}
+
+Write-Host "zen binary: $ZenBinary"
+Write-Host "zenserver binary: $ZenServerBinary"
+Write-Host "service name: $ServiceName"
+Write-Host ""
+
+try {
+
+# ── Test: status before install (should fail) ─────────────────────
+
+Write-Host "--- Test: status before install ---"
+
+$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String
+if ($Output -match "not installed") {
+ Pass "status reports 'not installed' for non-existent service"
+} else {
+ Fail "status should report 'not installed'" "got: $Output"
+}
+
+# ── Test: install ─────────────────────────────────────────────────
+
+Write-Host "--- Test: install ---"
+
+$Output = & $ZenBinary service install $ZenServerBinary $ServiceName --allow-elevation 2>&1 | Out-String
+$ExitCode = $LASTEXITCODE
+
+if ($ExitCode -eq 0) {
+ Pass "install exits with code 0"
+} else {
+ Fail "install exits with code 0" "got exit code: $ExitCode, output: $Output"
+}
+
+$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+if ($svc) {
+ Pass "service registered in SCM"
+} else {
+ Fail "service registered in SCM"
+}
+
+# Verify service configuration
+if ($svc) {
+ $svcWmi = Get-CimInstance -ClassName Win32_Service -Filter "Name='$ServiceName'" -ErrorAction SilentlyContinue
+ if ($svcWmi -and $svcWmi.PathName -match "zenserver") {
+ Pass "service binary path contains zenserver"
+ } else {
+ Fail "service binary path contains zenserver" "got: $($svcWmi.PathName)"
+ }
+
+ if ($svcWmi -and $svcWmi.StartMode -eq "Auto") {
+ Pass "service is set to auto-start"
+ } else {
+ Fail "service is set to auto-start" "got: $($svcWmi.StartMode)"
+ }
+}
+
+# ── Test: install again (already installed, no --full) ────────────
+
+Write-Host "--- Test: install again (idempotent) ---"
+
+$Output = & $ZenBinary service install $ZenServerBinary $ServiceName --allow-elevation 2>&1 | Out-String
+$ExitCode = $LASTEXITCODE
+
+if ($ExitCode -eq 0) {
+ Pass "re-install exits with code 0"
+} else {
+ Fail "re-install exits with code 0" "got exit code: $ExitCode"
+}
+
+if ($Output -match "already installed") {
+ Pass "re-install reports service already installed"
+} else {
+ Fail "re-install reports service already installed" "got: $Output"
+}
+
+# ── Test: status after install (not yet started) ──────────────────
+
+Write-Host "--- Test: status after install (stopped) ---"
+
+$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String
+if ($Output -match "not running") {
+ Pass "status reports 'not running' for stopped service"
+} else {
+ Fail "status reports 'not running' for stopped service" "got: $Output"
+}
+
+# ── Test: start ───────────────────────────────────────────────────
+
+Write-Host "--- Test: start ---"
+
+$Output = & $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-String
+$ExitCode = $LASTEXITCODE
+
+if ($ExitCode -eq 0) {
+ Pass "start exits with code 0"
+} else {
+ Fail "start exits with code 0" "got exit code: $ExitCode, output: $Output"
+}
+
+Start-Sleep -Seconds 2
+
+$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+if ($svc -and $svc.Status -eq "Running") {
+ Pass "service is running after start"
+
+ # ── Test: status while running ────────────────────────────────
+
+ Write-Host "--- Test: status (running) ---"
+
+ $Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String
+ if ($Output -match "Running") {
+ Pass "status reports 'Running'"
+ } else {
+ Fail "status reports 'Running'" "got: $Output"
+ }
+
+ if ($Output -match "zenserver") {
+ Pass "status shows executable path"
+ } else {
+ Fail "status shows executable path" "got: $Output"
+ }
+
+ # ── Test: start again (already running) ───────────────────────
+
+ Write-Host "--- Test: start again (already running) ---"
+
+ $Output = & $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-String
+ if ($Output -match "already running") {
+ Pass "start reports service already running"
+ } else {
+ Fail "start reports service already running" "got: $Output"
+ }
+
+ # ── Test: stop ────────────────────────────────────────────────
+
+ Write-Host "--- Test: stop ---"
+
+ $Output = & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-String
+ $ExitCode = $LASTEXITCODE
+
+ if ($ExitCode -eq 0) {
+ Pass "stop exits with code 0"
+ } else {
+ Fail "stop exits with code 0" "got exit code: $ExitCode, output: $Output"
+ }
+
+ Start-Sleep -Seconds 2
+
+ $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+ if ($svc -and $svc.Status -ne "Running") {
+ Pass "service is not running after stop"
+ } else {
+ Fail "service is not running after stop"
+ }
+} else {
+ Fail "service is running after start" "(skipping start-dependent tests)"
+}
+
+# ── Test: stop when already stopped ───────────────────────────────
+
+Write-Host "--- Test: stop when already stopped ---"
+
+Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
+Start-Sleep -Seconds 1
+
+$Output = & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-String
+if ($Output -match "not running") {
+ Pass "stop reports 'not running' when already stopped"
+} else {
+ Fail "stop reports 'not running' when already stopped" "got: $Output"
+}
+
+# ── Test: uninstall while running (should fail) ───────────────────
+
+Write-Host "--- Test: uninstall while running (should fail) ---"
+
+& $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-Null
+Start-Sleep -Seconds 2
+
+$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+if ($svc -and $svc.Status -eq "Running") {
+ $Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String
+ $ExitCode = $LASTEXITCODE
+
+ if ($ExitCode -ne 0 -or $Output -match "running.*stop") {
+ Pass "uninstall refuses while service is running"
+ } else {
+ Fail "uninstall refuses while service is running" "got: exit=$ExitCode, output: $Output"
+ }
+
+ # Stop it for the real uninstall test
+ & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-Null
+ Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
+ Start-Sleep -Seconds 2
+} else {
+ Write-Host " (skipped: could not start service for this test)"
+}
+
+# ── Test: uninstall ───────────────────────────────────────────────
+
+Write-Host "--- Test: uninstall ---"
+
+Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
+Start-Sleep -Seconds 1
+
+$Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String
+$ExitCode = $LASTEXITCODE
+
+if ($ExitCode -eq 0) {
+ Pass "uninstall exits with code 0"
+} else {
+ Fail "uninstall exits with code 0" "got exit code: $ExitCode, output: $Output"
+}
+
+$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+if (-not $svc) {
+ Pass "service removed from SCM after uninstall"
+} else {
+ Fail "service removed from SCM after uninstall"
+}
+
+# ── Test: status after uninstall ──────────────────────────────────
+
+Write-Host "--- Test: status after uninstall ---"
+
+$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String
+if ($Output -match "not installed") {
+ Pass "status reports 'not installed' after uninstall"
+} else {
+ Fail "status reports 'not installed' after uninstall" "got: $Output"
+}
+
+# ── Test: uninstall when not installed (idempotent) ───────────────
+
+Write-Host "--- Test: uninstall when not installed ---"
+
+$Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String
+$ExitCode = $LASTEXITCODE
+
+if ($ExitCode -eq 0) {
+ Pass "uninstall of non-existent service exits with code 0"
+} else {
+ Fail "uninstall of non-existent service exits with code 0" "got exit code: $ExitCode"
+}
+
+if ($Output -match "not installed") {
+ Pass "uninstall reports service not installed"
+} else {
+ Fail "uninstall reports service not installed" "got: $Output"
+}
+
+} finally {
+ Cleanup
+}
+
+if ($Script:Failed -gt 0) {
+ exit 1
+}