Files
kappa/test-init-switch.sh
T
huntedbytheirs dd984f96d4 feat: system-agnostic package manager — 5 inits, 2 bootloaders, parallel scheduler
Complete rewrite of kappa from a sequential build tool into a
system-agnostic package manager with runtime init switching.

Core additions:
- 5 init system backends: systemd, openrc, s6, runit, dinit
  (service file generation, enable/disable, init_paths)
- 2 bootloader backends: grub, limine (config generation, fallback entries)
- Parallel scheduler with worker pool, depth-based priority (Beta/Alpha/Zeta),
  atomic claiming, dependency tracking, deduplication, and failure propagation
- Init-switch impact analysis: only rebuild packages using ${enabledinit}
- Init-agnostic service definitions: flat NamedService blocks replace
  per-init nesting
- Package conflicts: mutual incompatibility detection in resolver
- System groups: init-agnostic group creation in DSL
- Init-agnostic hostname/timezone: direct /etc/hostname and /etc/localtime writes
- Source tarball caching at /kappa/cache/ with atomic write-then-rename
- Package recipe caching with remote fetching and version comparison
- remotes = [...] block in system config for package repositories
- Auto-fetch: rebuild resolves missing packages from remotes
- uninstall phase in package definitions
- ${enabledinit} eval variable for init-conditional builds
- Shared util module (to_lower, shell_escape)
- 54 integration tests across two shell test suites
- Comprehensive README and CONTRIBUTING guide

Bug fixes from review:
- CRITICAL: Replace std::system() with fork+execvp (command injection)
- CRITICAL: Fix scheduler deadlock on successful completion
- CRITICAL: Fix rebuild init/kernel/bootloader change detection
- HIGH: Fix path traversal via unsanitized package names in cache
- HIGH: Fix TOCTOU race in cache write with atomic rename
- HIGH: Fix formatter dropping remotes/imports blocks
- HIGH: Fix formatter stripping empty-string assert values
- HIGH: Fix formatter non-idempotent output (sorted key iteration)
- HIGH: Populate ${enabledinit} from boot.init in BuildStep
- MEDIUM: Fix data race on non-atomic scheduler stop flag
- MEDIUM: Fix compute_depths() traversal direction
- MEDIUM: Add runit to doctor supported-init warning
- MEDIUM: Extract to_lower/shell_escape to shared kappa::util
- MEDIUM: Consolidate generator declarations in headers
2026-07-31 05:32:57 -04:00

351 lines
9.4 KiB
Bash
Executable File

#!/bin/bash
# kappa init-switching integration test via systemd-nspawn
# Requires: systemd-nspawn, debootstrap (or pacstrap), kappa binary
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
KAPPA_BIN="${KAPPA_BIN:-./build/kappa}"
ROOTFS="./test-rootfs"
PASS=0
FAIL=0
cleanup() {
echo ""
echo "Cleaning up..."
rm -rf "$ROOTFS" /tmp/kappa-test-*.kap 2>/dev/null || true
}
trap cleanup EXIT
check() {
local desc="$1" cmd="$2" expect="$3"
echo -n " $desc ... "
local out
out=$(eval "$cmd" 2>&1) || true
if echo "$out" | grep -q "$expect"; then
echo -e "${GREEN}OK${NC}"
PASS=$((PASS + 1))
else
echo -e "${RED}FAIL${NC}"
echo " expected: $expect"
echo " got: $(echo "$out" | head -3 | tr '\n' ' ')"
FAIL=$((FAIL + 1))
fi
}
echo ""
echo "============================================"
echo " kappa init-switching integration tests"
echo "============================================"
echo ""
# ------------------------------------------------------------------
# 1. Create a minimal rootfs directory structure
# ------------------------------------------------------------------
echo "--- Setting up test rootfs ---"
mkdir -p "$ROOTFS"/{etc,usr/local/bin,kappa/{boot,db,store,temp},var/service}
cp "$KAPPA_BIN" "$ROOTFS/usr/local/bin/kappa"
chmod +x "$ROOTFS/usr/local/bin/kappa"
export KAPPA_ROOT="$ROOTFS/kappa"
# Create a minimal package registry (simulating installed packages)
mkdir -p "$KAPPA_ROOT/db"
# Simulate an installed system with nginx (has services), zlib (no services),
# and postgresql (multi-service, uses ${enabledinit})
cat > /tmp/kappa-test-nginx.kap << 'EOF'
package "nginx" {
version = "1.24"
source = "https://nginx.org/nginx-1.24.tar.gz"
service {
exec = "/usr/bin/nginx"
type = "forking"
ports = [80, 443]
user = "www-data"
description = "Nginx web server"
}
build { ./configure --prefix=${prefix}; make -j${jobs} }
install { make DESTDIR=${destdir} install }
}
EOF
cat > /tmp/kappa-test-zlib.kap << 'EOF'
package "zlib" {
version = "1.3"
source = "https://zlib.net/zlib-1.3.tar.gz"
build { ./configure --prefix=${prefix}; make -j${jobs} }
install { make DESTDIR=${destdir} install }
}
EOF
cat > /tmp/kappa-test-postgres.kap << 'EOF'
package "postgresql" {
version = "16.3"
source = "https://ftp.postgresql.org/source/postgresql-16.3.tar.gz"
service main {
exec = "/usr/bin/postgres"
type = "forking"
ports = [5432]
user = "postgres"
description = "PostgreSQL database server"
}
service checkpointer {
exec = "/usr/bin/postgres-checkpointer"
type = "longrun"
user = "postgres"
}
build {
case ${enabledinit} in
systemd) ./configure --with-systemd --prefix=${prefix} ;;
*) ./configure --prefix=${prefix} ;;
esac
make -j${jobs}
}
install { make DESTDIR=${destdir} install }
}
EOF
# Write installed DB (simulate nginx, zlib, postgresql already installed)
# Format: name version hash [provides...]
mkdir -p "$(dirname "$KAPPA_ROOT/db/installed")"
cat > "$KAPPA_ROOT/db/installed" << 'EOF'
nginx 1.24 a1b2c3d4e5f6a7b8
zlib 1.3 b2c3d4e5f6a7b8c9
postgresql 16.3 c3d4e5f6a7b8c9d0
init a1b2c3d4e5f6a7b8
kernel d4e5f6a7b8c9d0e1
bootloader e5f6a7b8c9d0e1f2
EOF
echo ""
# ------------------------------------------------------------------
# 2. Init system switching tests
# ------------------------------------------------------------------
echo "--- Init system switching ---"
# Base config template
config_template() {
local init="$1"
cat << EOF
system { hostname = "kappa-test" }
packages {
nginx {}
zlib {}
postgresql {}
$init {}
}
services {
nginx { enable = true }
postgresql { enable = true }
postgresql.checkpointer { enable = true }
}
boot {
kernel = "linux"
init = "$init"
root = "/dev/sda1"
bootloader = "limine"
}
groups {
wheel { gid = 998 }
}
users {
root { shell = "/bin/zsh" }
}
EOF
}
# Test systemd config
config_template "systemd" > /tmp/kappa-test-systemd.kap
check "parse systemd config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
'systemd — system and service manager'
check "systemd bootloader in config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
'Limine'
# Test openrc config
config_template "openrc" > /tmp/kappa-test-openrc.kap
check "parse openrc config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-openrc.kap" \
'OpenRC — dependency-based init'
# Test s6 config
config_template "s6" > /tmp/kappa-test-s6.kap
check "parse s6 config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-s6.kap" \
's6 — s6 supervision suite'
# Test runit config
config_template "runit" > /tmp/kappa-test-runit.kap
check "parse runit config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-runit.kap" \
'runit — supervision suite'
# Test dinit config
config_template "dinit" > /tmp/kappa-test-dinit.kap
check "parse dinit config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-dinit.kap" \
'dinit — service manager'
echo ""
# ------------------------------------------------------------------
# 3. Service generation verification
# ------------------------------------------------------------------
echo "--- Service file generation ---"
# Verify service specs are parsed correctly per init
for init in systemd openrc s6 runit dinit; do
check "service block present for $init config" \
"$KAPPA_BIN format /tmp/kappa-test-$init.kap" \
'enable = true'
done
# Verify the format output contains correct service fields
check "nginx service has type forking" \
"$KAPPA_BIN format /tmp/kappa-test-nginx.kap" \
'type = "forking"'
check "postgresql has named service checkpointer" \
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
'service checkpointer'
check "postgresql build uses enabledinit" \
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
'enabledinit'
echo ""
# ------------------------------------------------------------------
# 4. Rebuild impact analysis
# ------------------------------------------------------------------
echo "--- Rebuild impact analysis ---"
# Write an initial installed state matching s6 config
# (reuse the same installed DB from above)
config_template "s6" > /tmp/kappa-test-rebuild-s6.kap
# The packages list includes "s6" which isn't in the installed DB,
# so rebuild correctly shows it as a new package to build.
check "rebuild detects new init package" \
"$KAPPA_BIN rebuild /tmp/kappa-test-rebuild-s6.kap 2>&1" \
'packages to rebuild'
# Now test switching FROM s6 TO systemd
config_template "systemd" > /tmp/kappa-test-rebuild-systemd.kap
# The rebuild should detect init_changed
# Note: this requires the installed DB to have the old init hash
check "rebuild detects init change" \
"$KAPPA_BIN rebuild /tmp/kappa-test-rebuild-systemd.kap 2>&1" \
'packages to rebuild'
echo ""
# ------------------------------------------------------------------
# 5. Bootloader switching tests
# ------------------------------------------------------------------
echo "--- Bootloader switching ---"
for bl in grub limine; do
cat > "/tmp/kappa-test-bl-$bl.kap" << KAPEOF
system { hostname = "test" }
packages { nginx {} }
services {}
boot {
kernel = "linux"
init = "s6"
root = "/dev/sda1"
bootloader = "$bl"
}
users { root { shell = "/bin/sh" } }
KAPEOF
check "bootloader $bl recognized in config" \
"$KAPPA_BIN parse-config /tmp/kappa-test-bl-$bl.kap" \
"$bl"
done
# Test unknown bootloader
cat > /tmp/kappa-test-badbl.kap << 'KAPEOF'
system { hostname = "test" }
packages {}
services {}
boot {
kernel = "linux"
init = "s6"
root = "/dev/sda1"
bootloader = "lilo"
}
users { root { shell = "/bin/sh" } }
KAPEOF
check "doctor warns on unknown bootloader" \
"$KAPPA_BIN doctor /tmp/kappa-test-badbl.kap 2>&1" \
'not a recognized bootloader'
echo ""
# ------------------------------------------------------------------
# 6. Groups and conflicts
# ------------------------------------------------------------------
echo "--- Groups and conflicts ---"
cat > /tmp/kappa-test-groups2.kap << 'KAPEOF'
system { hostname = "test" }
packages {}
services {}
boot {
kernel = "linux"
init = "s6"
root = "/dev/sda1"
bootloader = "limine"
}
groups {
wheel { gid = 998 }
audio {}
docker { gid = 995 }
}
users { root { shell = "/bin/sh" } }
KAPEOF
check "groups with gid format correctly" \
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
'gid = 998'
check "groups without gid format as empty" \
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
'audio {}'
# Conflicts test
cat > /tmp/kappa-test-conflict-pkg.kap << 'KAPEOF'
package "systemd" {
version = "255"
source = "https://example.com/systemd.tar.gz"
provides = ["udev", "logind"]
conflicts = ["eudev", "elogind"]
build { make }
install { make install }
}
KAPEOF
check "conflicts parse correctly" \
"$KAPPA_BIN parse-package /tmp/kappa-test-conflict-pkg.kap" \
'valid'
check "conflicts format round-trips" \
"$KAPPA_BIN format /tmp/kappa-test-conflict-pkg.kap" \
'conflicts = \['
echo ""
echo "============================================"
echo -e " Results: ${GREEN}$PASS passed${NC}, ${RED}$FAIL failed${NC}"
echo "============================================"
[ "$FAIL" -eq 0 ] || exit 1