]> Nutra Git (v1) - gamesguru/git-remote-gcrypt.git/commitdiff
feat: Add Makefile, CI, and improved testing infrastructure feat/infrastructure
authorShane Jaroch <chown_tee@proton.me>
Thu, 1 Jan 2026 04:35:42 +0000 (23:35 -0500)
committerShane Jaroch <chown_tee@proton.me>
Thu, 1 Jan 2026 07:36:24 +0000 (02:36 -0500)
Adds a Makefile for standardizing test/lint/install workflows, a GitHub Actions CI workflow, and coverage reporting tools. Also updates install.sh to support version detection.

Signed-off-by: Shane Jaroch <chown_tee@proton.me>
.github/workflows/ci.yaml [new file with mode: 0644]
.gitignore
Makefile [new file with mode: 0644]
git-remote-gcrypt
graph.txt [new file with mode: 0644]
install.sh
tests/coverage_report.py [new file with mode: 0644]
tests/system-test.sh
tests/test-install-logic.sh [new file with mode: 0755]
tests/verify-system-install.sh [new file with mode: 0644]

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644 (file)
index 0000000..62e1af8
--- /dev/null
@@ -0,0 +1,102 @@
+---
+name: CI
+
+"on":
+  push:
+  workflow_dispatch:
+    inputs:
+      debug:
+        description: 'Enable debug logging (GCRYPT_DEBUG=1)'
+        required: false
+        type: boolean
+        default: false
+  schedule:
+    - cron: "0 0 * * 0" # Sunday at 12 AM
+
+jobs:
+  # Handles Ubuntu and macOS
+  install-unix:
+    runs-on: ${{ matrix.os }}
+
+    env:
+      GCRYPT_DEBUG: ${{ inputs.debug && '1' || '' }}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, macos-latest]
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Dependencies (Linux)
+        if: runner.os == 'Linux'
+        run: sudo apt-get update && sudo apt-get install -y git python3-docutils
+
+      - name: Dependencies (macOS)
+        if: runner.os == 'macOS'
+        run: brew install coreutils python3 git docutils
+
+      - name: Help [make]
+        run: make
+
+      - name: Test Installer
+        run: bash ./tests/test-install-logic.sh
+
+      - name: Install [make install]
+        run: sudo make install
+
+      - name: Verify [make check/install]
+        run: make check/install
+
+
+  # Handles RedHat (UBI Container)
+  install-rh:
+    runs-on: ubuntu-latest
+
+    env:
+      GCRYPT_DEBUG: ${{ inputs.debug && '1' || '' }}
+
+    container:
+      image: registry.access.redhat.com/ubi9/ubi:latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      # dnf is slow in containers. We cache the dnf cache directory.
+      - name: Cache DNF
+        uses: actions/cache@v4
+        with:
+          path: /var/cache/dnf
+          key: ${{ runner.os }}-ubi9-dnf-v1
+          restore-keys: |
+            ${{ runner.os }}-ubi9-dnf-
+
+      - name: Dependencies [redhat]
+        run: dnf install -y git python3-docutils make man-db
+
+      - name: Help [make]
+        run: make
+
+      - name: Test Installer
+        run: bash ./tests/test-install-logic.sh
+
+      - name: Install [make install]
+        run: make install # container runs as sudo
+
+      - name: Verify [make check/install]
+        run: make check/install
+
+
+  # Lint job (no-op currently)
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install ShellCheck
+        run: sudo apt-get update && sudo apt-get install -y shellcheck
+
+      - name: Lint [make lint]
+        continue-on-error: true
+        run: make lint
index 2395a05ab41e908912a3d7adb592cf357935d900..d6d106f4094f064245687881f1677e68b589b9a0 100644 (file)
@@ -1,3 +1,11 @@
 /debian/files
 /debian/git-remote-gcrypt.substvars
 /debian/git-remote-gcrypt
+
+# Test coverage
+.coverage/
+!.coverage/**
+# scratch pad
+.tmp/
+!.tmp/**
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..a1d9670
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,160 @@
+SHELL:=/bin/bash
+# .ONESHELL:
+# .EXPORT_ALL_VARIABLES:
+.DEFAULT_GOAL := _help
+.SHELLFLAGS = -ec
+
+.PHONY: _help
+_help:
+       @printf "\nUsage: make <command>, valid commands:\n\n"
+       @awk 'BEGIN {FS = ":.*?##H "}; \
+               /##H/ && !/@awk.*?##H/ { \
+                       target=$$1; doc=$$2; \
+                       if (length(target) > max) max = length(target); \
+                       targets[NR] = target; docs[NR] = doc; list[NR] = 1; \
+               } \
+               END { \
+                       for (i = 1; i <= NR; i++) { \
+                               if (list[i]) printf "  \033[1;34m%-*s\033[0m  %s\n", max, targets[i], docs[i]; \
+                       } \
+                       print ""; \
+               }' $(MAKEFILE_LIST)
+
+.PHONY: _all
+_all: lint test/installer test/system
+       @$(call print_success,All checks passed!)
+
+.PHONY: vars
+vars:  ##H Debug: Print project variables
+       @$(foreach v,$(sort $(.VARIABLES)), \
+               $(if $(filter file command line override,$(origin $(v))), \
+                       $(info $(v) = $($(v))) \
+               ) \
+       )
+
+define print_err
+       printf "\033[1;31m%s\033[0m\n" "$(1)"
+endef
+
+define print_warn
+       printf "\033[1;33m%s\033[0m\n" "$(1)"
+endef
+
+define print_success
+       printf "\033[1;34m✓ %s\033[0m\n" "$(1)"
+endef
+
+define print_info
+       printf "\033[1;36m%s\033[0m\n" "$(1)"
+endef
+
+
+.PHONY: check/deps
+check/deps:    ##H Verify kcov & shellcheck
+       @command -v shellcheck >/dev/null 2>&1 || { $(call print_err,Error: 'shellcheck' not installed.); exit 1; }
+       @$(call print_info,  --- shellcheck version ---) && shellcheck --version
+       @command -v kcov >/dev/null 2>&1 || { $(call print_err,Error: 'kcov' not installed.); exit 1; }
+       @$(call print_info,  --- kcov version ---) && kcov --version
+       @$(call print_success,Dependencies OK.)
+
+
+.PHONY: lint
+lint:  ##H Run shellcheck
+       # lint install script
+       shellcheck install.sh
+       @$(call print_success,OK.)
+       # lint system/binary script
+       shellcheck git-remote-gcrypt
+       @$(call print_success,OK.)
+       # lint test scripts
+       shellcheck tests/*.sh
+       @$(call print_success,OK.)
+
+# --- Test Config ---
+PWD := $(shell pwd)
+COV_ROOT    := $(PWD)/.coverage
+COV_SYSTEM  := $(COV_ROOT)/system
+COV_INSTALL := $(COV_ROOT)/installer
+
+.PHONY: test/, test
+test/: test
+test: test/installer test/system test/cov      ##H All tests & coverage
+
+.PHONY: test/installer
+test/installer: check/deps     ##H Test installer logic
+       @rm -rf $(COV_INSTALL)
+       @mkdir -p $(COV_INSTALL)
+       @export COV_DIR=$(COV_INSTALL); \
+        kcov --bash-handle-sh-invocation \
+            --include-pattern=install.sh \
+            --exclude-path=$(PWD)/.git,$(PWD)/tests \
+            $(COV_INSTALL) \
+            ./tests/test-install-logic.sh
+
+
+.PHONY: test/system
+test/system: check/deps        ##H Test system functionality
+       @rm -rf $(COV_SYSTEM)
+       @mkdir -p $(COV_SYSTEM)
+       @export GPG_TTY=$$(tty); \
+        [ -n "$(DEBUG)$(V)" ] && export GCRYPT_DEBUG=1 && print_warn "Debug mode enabled"; \
+        export GIT_CONFIG_PARAMETERS="'gcrypt.gpg-args=--pinentry-mode loopback --no-tty'"; \
+        export COV_DIR=$(COV_SYSTEM); \
+        for test_script in tests/system-test*.sh; do \
+            kcov --include-path=$(PWD) \
+                 --include-pattern=git-remote-gcrypt \
+                 --exclude-path=$(PWD)/.git,$(PWD)/tests \
+                 $(COV_SYSTEM) \
+                 ./$$test_script || true; \
+        done
+
+
+define CHECK_COVERAGE
+@XML_FILE=$$(find $(1) -name "cobertura.xml" 2>/dev/null | grep "merged" | head -n 1); \
+[ -z "$$XML_FILE" ] && XML_FILE=$$(find $(1) -name "cobertura.xml" 2>/dev/null | head -n 1); \
+if [ -f "$$XML_FILE" ]; then \
+       echo ""; \
+       echo "Report for: file://$$(dirname "$$XML_FILE")/index.html"; \
+       XML_FILE="$$XML_FILE" PATT="$(2)" python3 tests/coverage_report.py; \
+       fi
+endef
+
+.PHONY: test/cov
+test/cov:      ##H Show coverage gaps
+       $(call CHECK_COVERAGE,$(COV_SYSTEM),git-remote-gcrypt)
+       $(call CHECK_COVERAGE,$(COV_INSTALL),install.sh)
+
+
+
+.PHONY: install/, install
+install/: install
+install:       ##H Install system-wide
+       @$(call print_target,install)
+       @$(call print_info,Installing git-remote-gcrypt...)
+       @bash ./install.sh
+       @$(call print_success,Installed.)
+
+.PHONY: install/user
+install/user:  ##H make install prefix=~/.local
+       $(MAKE) install prefix=~/.local
+
+.PHONY: check/install
+check/install: ##H Verify installation works
+       bash ./tests/verify-system-install.sh
+
+.PHONY: uninstall/, uninstall
+uninstall/: uninstall
+uninstall:     ##H Uninstall
+       @$(call print_target,uninstall)
+       @bash ./uninstall.sh
+       @$(call print_success,Uninstalled.)
+
+.PHONY: uninstall/user
+uninstall/user:        ##H make uninstall prefix=~/.local
+       $(MAKE) uninstall prefix=~/.local
+
+
+
+.PHONY: clean
+clean: ##H Clean up
+       rm -rf .coverage
index ccb374e4ca1379d15880769210da70cb44e862c5..550e8c290eab8b720cd8f3d896233bb40e36561b 100755 (executable)
@@ -25,6 +25,12 @@ set -e # errexit
 set -f # noglob
 set -C # noclobber
 
+VERSION="@@DEV_VERSION@@"
+if [ "${1:-}" = "-v" ] || [ "${1:-}" = "--version" ]; then
+       echo "git-remote-gcrypt version $VERSION" >&2
+       exit 0
+fi
+
 export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked
 Gref="refs/gcrypt/gitception$GITCEPTION"
 Gref_rbranch="refs/heads/master"
@@ -366,7 +372,7 @@ PRIVDECRYPT()
 {
        local status_=
        exec 4>&1 &&
-       status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4 || { 
+       status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4 || {
                rc=$?
                print_debug "rungpg failed with exit code $rc"
                echo_info "ignoring GPG errors (likely anonymous recipient OR pinentry)."
@@ -865,8 +871,8 @@ EOF
        done <<EOF
 $1
 EOF
-       
-       echo_git 
+
+       echo_git
 }
 
 cleanup_tmpfiles()
diff --git a/graph.txt b/graph.txt
new file mode 100644 (file)
index 0000000..c1cad08
--- /dev/null
+++ b/graph.txt
@@ -0,0 +1,10 @@
+* 416fad4 (HEAD -> feat/user-experience) feat: Add shell completions, uninstall script, and CLI flags
+* 6fcafc9 (feat/infrastructure) feat: Add Makefile, CI, and improved testing infrastructure
+* 88a122e (fix/gpg-checksum) fix(gpg): Handle ECDH checksum error with many keys
+* ee1494b (gg/master, gg/HEAD, master, hotfix-01, backup-mega-commit) fix(gpg): Handle ECDH checksum error with many keys
+| * 0d66303 (gg/hotfix-01) debug workflows
+| * cffb09e fixup! add -h/--help target
+| * e699981 add completions
+| * 4e618f3 add -h/--help target
+| * 7ac8613 add uninstall script/target
+| * b85ed29 fixup! debug logging
index 7fc1cfcf4cb4440d0da8c30e5cc593cd7ed675eb..3cd6fd3a96497e909b8b641c30b25dcfc19d0600 100755 (executable)
@@ -1,33 +1,78 @@
 #!/bin/sh
-
 set -e
 
-: ${prefix:=/usr/local}
-: ${DESTDIR:=}
+: "${prefix:=/usr/local}"
+: "${DESTDIR:=}"
 
 verbose() { echo "$@" >&2 && "$@"; }
-install_v()
-{
+
+install_v() {
        # Install $1 into $2/ with mode $3
        verbose install -d "$2" &&
-       verbose install -m "$3" "$1" "$2"
+               verbose install -m "$3" "$1" "$2"
 }
 
-install_v git-remote-gcrypt "$DESTDIR$prefix/bin" 755
+# --- VERSION DETECTION ---
+if [ -f /etc/os-release ]; then
+       . /etc/os-release
+       OS_IDENTIFIER=$ID # Linux
+elif command -v uname >/dev/null; then
+       # Fallback for macOS/BSD (darwin)
+       OS_IDENTIFIER=$(uname -s | tr '[:upper:]' '[:lower:]')
+else
+       OS_IDENTIFIER="unknown_OS"
+fi
+
+# Get base version then append OS identifier
+if [ -d .git ] && command -v git >/dev/null; then
+       VERSION=$(git describe --tags --always --dirty 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo "sha_unknown")
+else
+       if [ ! -f debian/changelog ]; then
+               echo "Error: debian/changelog not found (and not a git repo)" >&2
+               exit 1
+       fi
+       VERSION=$(grep ^git-remote-gcrypt debian/changelog | head -n 1 | awk '{print $2}' | tr -d '()')
+fi
+VERSION="$VERSION (deb running on $OS_IDENTIFIER)"
+
+echo "Detected version: $VERSION"
 
-if command -v rst2man >/dev/null
-then
+# Setup temporary build area
+BUILD_DIR="./.build_tmp"
+mkdir -p "$BUILD_DIR"
+trap 'rm -rf "$BUILD_DIR"' EXIT
+
+# Placeholder injection
+sed "s/@@DEV_VERSION@@/$VERSION/g" git-remote-gcrypt >"$BUILD_DIR/git-remote-gcrypt"
+
+# --- INSTALLATION ---
+# This is where the 'Permission denied' happens if not sudo
+install_v "$BUILD_DIR/git-remote-gcrypt" "$DESTDIR$prefix/bin" 755
+
+if command -v rst2man >/dev/null; then
        rst2man='rst2man'
-elif command -v rst2man.py >/dev/null # it is installed as rst2man.py on macOS
-then
+elif command -v rst2man.py >/dev/null; then # it is installed as rst2man.py on macOS
        rst2man='rst2man.py'
 fi
 
-if [ -n "$rst2man" ]
-then
-       trap 'rm -f git-remote-gcrypt.1.gz' EXIT
-       verbose $rst2man ./README.rst | gzip -9 > git-remote-gcrypt.1.gz
+if [ -n "$rst2man" ]; then
+       # Update trap to clean up manpage too
+       trap 'rm -rf "$BUILD_DIR"; rm -f git-remote-gcrypt.1.gz' EXIT
+       verbose "$rst2man" ./README.rst | gzip -9 >git-remote-gcrypt.1.gz
        install_v git-remote-gcrypt.1.gz "$DESTDIR$prefix/share/man/man1" 644
 else
        echo "'rst2man' not found, man page not installed" >&2
 fi
+
+# Suggest installing shell completions
+cat >&2 <<EOF
+
+Installation complete!
+
+Optional: Install shell completions for tab completion:
+  Bash:  completions/bash/git-remote-gcrypt
+  Zsh:   completions/zsh/_git-remote-gcrypt
+  Fish:  completions/fish/git-remote-gcrypt.fish
+
+See completions/README.rst for details.
+EOF
diff --git a/tests/coverage_report.py b/tests/coverage_report.py
new file mode 100644 (file)
index 0000000..ab19b1f
--- /dev/null
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Dec 31 08:57:33 2025
+
+@author: shane
+"""
+
+import os
+import textwrap
+import xml.etree.ElementTree as E
+
+xml_file = os.environ.get("XML_FILE")
+patt = os.environ.get("PATT")
+
+tree = E.parse(xml_file)
+missed = []
+total_lines = 0
+missed_lines = 0
+
+for c in tree.findall(".//class"):
+    if patt in c.get("filename", ""):
+        for line in c.findall(".//line"):
+            total_lines += 1
+            if line.get("hits") == "0":
+                missed.append(line.get("number"))
+                missed_lines += 1
+
+if total_lines > 0:
+    COVERED = total_lines - missed_lines
+    pct = (COVERED / total_lines) * 100
+    COLOR = "\033[32;1m" if pct > 80 else "\033[33;1m" if pct > 50 else "\033[31;1m"
+    print(f"{COLOR}Coverage: {pct:.1f}% ({COVERED}/{total_lines})\033[0m")
+else:
+    print(f"Coverage: N/A (0 lines found for {patt})")
+
+if missed:
+    print(f"\033[31;1m{len(missed)} missing lines\033[0m in {patt}:")
+    print(
+        textwrap.fill(
+            ", ".join(missed), width=72, initial_indent="  ", subsequent_indent="  "
+        )
+    )
index 74de3a534b78e3ff4cbcea042dd1a116328c0242..ecb0340aa5d325c42702814f0962e763bb489d57 100755 (executable)
@@ -4,6 +4,14 @@
 set -efuC -o pipefail
 shopt -s inherit_errexit
 
+# Helpers
+print_info() { printf "\033[1;36m%s\033[0m\n" "$1"; }
+print_success() { printf "\033[1;34m✓ %s\033[0m\n" "$1"; }
+print_err() { printf "\033[1;31m%s\033[0m\n" "$1"; }
+
+
+
+
 # Unlike the main git-remote-gcrypt program, this testing script requires bash
 # (rather than POSIX sh) and also depends on various common system utilities
 # that the git-remote-gcrypt carefully avoids using (such as mktemp(1)).
@@ -29,6 +37,8 @@ pack_size_limit="12m" # If this variable is unset, there is no size limit.
 readonly num_commits files_per_commit random_source random_data_per_file \
     default_branch test_user_name test_user_email pack_size_limit
 
+print_info "Running system test..."
+
 # Pipe text into this function to indent it with four spaces. This is used
 # to make the output of this script prettier.
 indent() {
@@ -44,8 +54,8 @@ section_break() {
 assert() {
     (set +e; [[ -n ${show_command:-} ]] && set -x; "${@}")
     local -r status=${?}
-    { [[ ${status} -eq 0 ]] && echo "Verification succeeded."; } || \
-        echo "Verification failed."
+    { [[ ${status} -eq 0 ]] && print_success "Verification succeeded."; } || \
+        print_err "Verification failed."
     return "${status}"
 }
 
@@ -111,7 +121,8 @@ random_data_file="${tempdir}/data"
 head -c "${random_data_size}" "${random_source}" > "${random_data_file}"
 
 # Create gpg key and subkey.
-echo "Step 1: Creating a new GPG key and subkey to use for testing:"
+# Create gpg key and subkey.
+print_info "Step 1: Creating a new GPG key and subkey to use for testing:"
 (
     set -x
     gpg --batch --passphrase "" --quick-generate-key \
@@ -122,7 +133,7 @@ echo "Step 1: Creating a new GPG key and subkey to use for testing:"
 ###
 section_break
 
-echo "Step 2: Creating new repository with random data:"
+print_info "Step 2: Creating new repository with random data:"
 {
     git init -- "${tempdir}/first"
     cd "${tempdir}/first"
@@ -154,14 +165,14 @@ echo "Step 2: Creating new repository with random data:"
 ###
 section_break
 
-echo "Step 3: Creating an empty bare repository to receive pushed data:"
+print_info "Step 3: Creating an empty bare repository to receive pushed data:"
 git init --bare -- "${tempdir}/second.git" | indent
 
 
 ###
 section_break
 
-echo "Step 4: Pushing the first repository to the second one using gitception:"
+print_info "Step 4: Pushing the first repository to the second one using gitception:"
 {
     # Note that when pushing to a bare local repository, git-remote-gcrypt uses
     # gitception, rather than treating the remote as a local repository.
@@ -197,7 +208,7 @@ echo "Step 4: Pushing the first repository to the second one using gitception:"
 ###
 section_break
 
-echo "Step 5: Cloning the second repository using gitception:"
+print_info "Step 5: Cloning the second repository using gitception:"
 {
     (
         set -x
@@ -221,3 +232,5 @@ echo "Step 5: Cloning the second repository using gitception:"
     show_command=1 assert diff -r --exclude ".git" -- \
         "${tempdir}/first" "${tempdir}/third" 2>&1 | indent
 } | indent
+
+[ -n "${COV_DIR:-}" ] && print_success "OK. Report: file://${COV_DIR}/index.html"
diff --git a/tests/test-install-logic.sh b/tests/test-install-logic.sh
new file mode 100755 (executable)
index 0000000..2fdc79c
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/bash
+set -u
+
+# 1. Setup Sandbox
+SANDBOX=$(mktemp -d)
+trap 'rm -rf "$SANDBOX"' EXIT
+
+# Helpers
+print_info() { printf "\033[1;36m[TEST] %s\033[0m\n" "$1"; }
+print_success() { printf "\033[1;34m[TEST] ✓ %s\033[0m\n" "$1"; }
+print_err() { printf "\033[1;31m[TEST] FAIL: %s\033[0m\n" "$1"; }
+
+print_info "Running install logic tests in $SANDBOX..."
+
+# 2. Copy artifacts
+cp git-remote-gcrypt "$SANDBOX"
+cp README.rst "$SANDBOX" 2>/dev/null || touch "$SANDBOX/README.rst"
+cp install.sh "$SANDBOX"
+cd "$SANDBOX" || exit 2
+
+# Ensure source binary has the placeholder for sed to work on
+# If your local git-remote-gcrypt already has a real version, sed won't find the tag
+if ! grep -q "@@DEV_VERSION@@" git-remote-gcrypt; then
+       echo 'VERSION="@@DEV_VERSION@@"' >git-remote-gcrypt
+fi
+chmod +x git-remote-gcrypt
+
+INSTALLER="./install.sh"
+
+assert_version() {
+       EXPECTED_SUBSTRING="$1"
+       PREFIX="$SANDBOX/usr"
+       export prefix="$PREFIX"
+       unset DESTDIR
+
+       # Run the installer
+       "$INSTALLER" >/dev/null 2>&1 || {
+               echo "Installer failed unexpectedly"
+               return 1
+       }
+
+       INSTALLED_BIN="$PREFIX/bin/git-remote-gcrypt"
+       chmod +x "$INSTALLED_BIN"
+
+       OUTPUT=$("$INSTALLED_BIN" --version 2>&1 </dev/null)
+
+       # CRITICAL: Use quotes around the variable to handle parentheses correctly
+       if [[ "$OUTPUT" != *"$EXPECTED_SUBSTRING"* ]]; then
+               print_err "FAILED: Expected '$EXPECTED_SUBSTRING' in output."
+               print_err "        Got: '$OUTPUT'"
+               exit 1
+       else
+               printf "  ✓ %s\n" "Found version '$EXPECTED_SUBSTRING'"
+       fi
+}
+
+# --- TEST 1: Strict Metadata Requirement ---
+echo "--- Test 1: Fail without Metadata ---"
+rm -rf debian redhat
+if "$INSTALLER" >/dev/null 2>&1; then
+       print_err "FAILED: Installer should have exited 1 without debian/changelog"
+       exit 1
+else
+       printf "  ✓ %s\n" "Installer strictly requires metadata"
+fi
+
+# --- TEST 2: Debian-sourced Versioning ---
+echo "--- Test 2: Versioning from Changelog ---"
+mkdir -p debian
+echo "git-remote-gcrypt (5.5.5-1) unstable; urgency=low" >debian/changelog
+
+# Determine the OS identifier for the test expectation
+if [ -f /etc/os-release ]; then
+       # shellcheck source=/dev/null
+       source /etc/os-release
+       OS_IDENTIFIER="$ID"
+elif command -v uname >/dev/null; then
+       OS_IDENTIFIER=$(uname -s | tr '[:upper:]' '[:lower:]')
+else
+       OS_IDENTIFIER="unknown_os"
+fi
+
+# Use the identified OS for the expected string
+EXPECTED_TAG="5.5.5-1 (deb running on $OS_IDENTIFIER)"
+
+assert_version "$EXPECTED_TAG"
+
+# --- TEST 3: DESTDIR Support ---
+echo "--- Test 3: DESTDIR Support ---"
+rm -rf "${SANDBOX:?}/usr"
+export DESTDIR="$SANDBOX/pkg_root"
+export prefix="/usr"
+
+"$INSTALLER" >/dev/null 2>&1
+
+if [ -f "$SANDBOX/pkg_root/usr/bin/git-remote-gcrypt" ]; then
+       printf "  ✓ %s\n" "DESTDIR honored"
+else
+       print_err "FAILED: Binary not found in DESTDIR"
+       exit 1
+fi
+
+print_success "All install logic tests passed."
+[ -n "${COV_DIR:-}" ] && print_success "OK. Report: file://${COV_DIR}/index.html"
+
+exit 0
diff --git a/tests/verify-system-install.sh b/tests/verify-system-install.sh
new file mode 100644 (file)
index 0000000..c84575b
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/bash
+set -u
+
+# Helpers
+print_info() { printf "\033[1;36m%s\033[0m\n" "$1"; }
+print_success() { printf "\033[1;34m✓ %s\033[0m\n" "$1"; }
+print_err() { printf "\033[1;31m%s\033[0m\n" "$1"; }
+
+print_info "Verifying system install..."
+
+# 1. Check if the command exists in the path
+if ! command -v git-remote-gcrypt >/dev/null; then
+       print_err "ERROR: git-remote-gcrypt is not in the PATH."
+       exit 1
+fi
+
+# 2. Run the version check (Capture stderr too!)
+OUTPUT=$(git-remote-gcrypt -v 2>&1)
+EXIT_CODE=$?
+
+if [ $EXIT_CODE -ne 0 ]; then
+       print_err "ERROR: Command exited with code $EXIT_CODE"
+       exit 1
+fi
+
+# 3. Verify the placeholder was replaced
+if [[ "$OUTPUT" == *"@@DEV_VERSION@@"* ]]; then
+       print_err "ERROR: Version placeholder @@DEV_VERSION@@ was not replaced!"
+       exit 1
+fi
+
+# 4. Determine expected ID for comparison to actual
+if [ -f /etc/os-release ]; then
+       # shellcheck source=/dev/null
+       source /etc/os-release
+       EXPECTED_ID=$ID
+elif command -v uname >/dev/null; then
+       EXPECTED_ID=$(uname -s | tr '[:upper:]' '[:lower:]')
+else
+       EXPECTED_ID="unknown_OS"
+fi
+
+if [[ "$OUTPUT" != *"(deb running on $EXPECTED_ID)"* ]]; then
+       print_err "ERROR: Distro ID '$EXPECTED_ID' missing from version string! (Got: $OUTPUT)"
+       exit 1
+fi
+
+# LEAD with the version success
+printf "  ✓ %s\n" "VERSION OK: $OUTPUT"
+
+# 5. Verify the man page
+if man -w git-remote-gcrypt >/dev/null 2>&1; then
+       printf "  ✓ %s\n" "DOCS OK: Man page is installed and indexed."
+else
+       print_err "ERROR: Man page not found in system paths."
+       exit 1
+fi
+
+print_success "INSTALLATION VERIFIED"