From d10679d267e74f600621c57f8ae6318d08219f11 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Tue, 4 Aug 2026 14:15:50 -0400 Subject: [PATCH] test: add 85 comprehensive tests and Gitea CI/CD workflow - test-full.sh: 85 tests covering CLI, DSL parsing (all keywords), config parsing, users/groups, assertions, imports, doctor diagnostics, repos/remotes, index generation, add/remove operations, install DB, resolver (conflicts + missing), scheduler (deptree ordering), build execution + error handling, env operators, init systems, bootloaders, variable interpolation, --root override, rollback config, system env, feature flags, rebuild dry-run - .gitea/workflows/ci.yml: builds on Ubuntu with clang+cmake+ninja, runs all three test suites on push/PR to main Total test count: 31 (DSL) + 23 (init-switch) + 85 (comprehensive) = 139 --- .gitea/workflows/ci.yml | 34 ++ test-full.sh | 992 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1026 insertions(+) create mode 100644 .gitea/workflows/ci.yml create mode 100755 test-full.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..ac98365 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt-get update -qq + apt-get install -y -qq clang cmake ninja-build + + - name: Build kappa + run: | + cmake -B build -G Ninja \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ + cmake --build build + + - name: DSL and CLI tests + run: ./test.sh + + - name: Init-switching tests + run: ./test-init-switch.sh + + - name: Comprehensive tests + run: ./test-full.sh diff --git a/test-full.sh b/test-full.sh new file mode 100755 index 0000000..4bf986e --- /dev/null +++ b/test-full.sh @@ -0,0 +1,992 @@ +#!/bin/sh +# kappa comprehensive test suite — DSL, ops, error handling, edge cases +set -e + +KAPPA="${KAPPA_BIN:-./build/kappa}" +PASS=0 +FAIL=0 + +check() { + local desc="$1" cmd="$2" expect="$3" + local out + out=$(eval "$cmd" 2>&1) || true + if echo "$out" | grep -q "$expect"; then + echo " ✓ $desc" + PASS=$((PASS + 1)) + else + echo " ✗ $desc" + echo " expected: $expect" + echo " got: $(echo "$out" | head -3 | tr '\n' ' ')" + FAIL=$((FAIL + 1)) + fi +} + +check_fail() { + local desc="$1" cmd="$2" expect="$3" + local out rc=0 + out=$(eval "$cmd" 2>&1) || rc=$? + if [ "$rc" -ne 0 ] && echo "$out" | grep -q "$expect"; then + echo " ✓ $desc" + PASS=$((PASS + 1)) + else + echo " ✗ $desc" + echo " expected exit≠0 with: $expect" + echo " exit: $rc, got: $(echo "$out" | head -3 | tr '\n' ' ')" + FAIL=$((FAIL + 1)) + fi +} + +TMPDIR=$(mktemp -d /tmp/kappa-full-test-XXXXX) +trap 'rm -rf $TMPDIR' EXIT +export KAPPA_ROOT="$TMPDIR/root" +mkdir -p "$KAPPA_ROOT"/{bin,temp,db,system,builds,cache/{packages,indexes}} + +# ═══════════════════════════════════════════ +echo "" +echo "=== CLI: help, version, error handling ===" +echo "" + +check "kappa --help" \ + "$KAPPA --help" \ + 'Usage' + +check "kappa -h" \ + "$KAPPA -h" \ + 'Subcommands' + +check "kappa -V" \ + "$KAPPA -V" \ + 'kappa 0.1.0' + +check_fail "missing subcommand" \ + "$KAPPA" \ + 'missing subcommand' + +check_fail "unknown subcommand" \ + "$KAPPA fakething" \ + "unknown subcommand 'fakething'" + +check_fail "missing file arg" \ + "$KAPPA parse-package" \ + 'no input file' + +check_fail "nonexistent file" \ + "$KAPPA parse-package /nonexistent/file.kap" \ + 'cannot open' + +# ═══════════════════════════════════════════ +echo "" +echo "=== DSL: parse-package and validate ===" +echo "" + +cat > "$TMPDIR/basic.kap" << 'KAPEOF' +package "testpkg" { + version = "1.0" + source = "https://example.com/test-1.0.tar.gz" + sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + license = "MIT" + provides = ["libtest.so.1"] + build { make -j${jobs} } + install { make DESTDIR=${destdir} install } +} +KAPEOF + +check "basic package parse" \ + "$KAPPA parse-package $TMPDIR/basic.kap" \ + 'package "testpkg" 1.0 — valid' + +check "validate package" \ + "$KAPPA validate $TMPDIR/basic.kap" \ + 'valid package' + + +check_fail "unclosed brace" \ + "printf 'package \"bad\" {\n version = \"1\"\n' | $KAPPA parse-package /dev/stdin" \ + 'unclosed' + +# ═══════════════════════════════════════════ +echo "" +echo "=== DSL: all keywords parse and format ===" +echo "" + +cat > "$TMPDIR/full.kap" << 'KAPEOF' +package "fullpkg" { + const version = "2.0" + const source = "https://example.com/full-v2.0.tar.gz" + sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + sha512 = "abc" + md5 = "def" + license = "BSD-2" + provides = ["full", "libfull.so"] + outputs = ["bin", "lib"] + conflicts = ["incompatible"] + depends = [{ name = "zlib", version = ">=1.2" }, "openssl"] + features { + ssl = { enabled = true, flag = "--with-ssl" } + debug = false + } + config { + file "etc/full.conf" mode = "default" { + port = "8080" + } + } + env { + CFLAGS = "-O2" + CFLAGS += "-pipe" + LDFLAGS ?= "-s" + } + patches = [{ url = "https://example.com/fix.patch", sha256 = "abc", level = 1 }] + service { + exec = "/usr/bin/full" + type = "forking" + ports = [80, 443] + } + assert { + "ssl required" : features.ssl == "true" + } + prepare { echo prepare } + build { make -j4 } + check { make check } + install { make DESTDIR=/tmp/dest install } +} +KAPEOF + +check "full keyword parse" \ + "$KAPPA parse-package $TMPDIR/full.kap" \ + 'package "fullpkg"' + +check "format full roundtrip" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'package "fullpkg"' + +check "format preserves provides" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'libfull.so' + +check "format preserves features" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'features {' + +check "format preserves env +=" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'CFLAGS +=' + +check "format preserves env ?=" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'LDFLAGS ?=' + +check "format preserves service" \ + "$KAPPA format $TMPDIR/full.kap" \ + '/usr/bin/full' + +check "format preserves asserts" \ + "$KAPPA format $TMPDIR/full.kap" \ + 'assert {' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: parse-config and format ===" +echo "" + +cat > "$TMPDIR/simple-config.kap" << 'KAPEOF' +system { + hostname = "test" +} +packages { + testpkg {} +} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "parse-config basic" \ + "$KAPPA parse-config $TMPDIR/simple-config.kap" \ + 'valid' + +check "parse-config shows init" \ + "$KAPPA parse-config $TMPDIR/simple-config.kap" \ + 's6' + +check "validate config" \ + "$KAPPA validate $TMPDIR/simple-config.kap" \ + 'valid system configuration' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: users and groups ===" +echo "" + +cat > "$TMPDIR/users-config.kap" << 'KAPEOF' +system { + hostname = "test" +} +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +groups { + wheel { gid = 998 } + audio {} +} +users { + root { + shell = "/bin/zsh" + groups = ["wheel", "audio"] + } +} +KAPEOF + +check "parse-config with users" \ + "$KAPPA parse-config $TMPDIR/users-config.kap" \ + '1 users' + +check "format users roundtrip" \ + "$KAPPA format $TMPDIR/users-config.kap" \ + '/bin/zsh' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: assertions ===" +echo "" + +cat > "$TMPDIR/assert-pass.kap" << 'KAPEOF' +system { hostname = "test" } +packages { testpkg {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +assert { + "root required" : boot.root != "" + "init must be s6" : boot.init == "s6" +} +KAPEOF + +check "validate passes assertions" \ + "$KAPPA validate $TMPDIR/assert-pass.kap" \ + 'valid system configuration' + +cat > "$TMPDIR/assert-fail.kap" << 'KAPEOF' +system { hostname = "test" } +packages {} +services {} +boot { + kernel = "linux" + init = "openrc" + root = "/dev/sda1" + bootloader = "limine" +} +assert { + "must be s6" : boot.init == "s6" +} +KAPEOF + +check_fail "validate fails assertion" \ + "$KAPPA validate $TMPDIR/assert-fail.kap" \ + 'assertion failures' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: imports ===" +echo "" + +cat > "$TMPDIR/imported.kap" << 'KAPEOF' +system { hostname = "imported.local" } +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +cat > "$TMPDIR/importer.kap" << 'KAPEOF' +imports = ["imported.kap"] +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "imports merges hostname" \ + "$KAPPA parse-config $TMPDIR/importer.kap" \ + 'valid' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Doctor diagnostics ===" +echo "" + +cat > "$TMPDIR/doctor-bad.kap" << 'KAPEOF' +package "bad" { + version = "" + source = "" + build { echo ok } + install { echo ok } +} +KAPEOF + +check_fail "doctor catches empty version" \ + "$KAPPA doctor $TMPDIR/doctor-bad.kap" \ + 'version is not set' + +check_fail "doctor catches empty source" \ + "$KAPPA doctor $TMPDIR/doctor-bad.kap" \ + 'source URL is not set' + +check_fail "doctor warns missing license" \ + "$KAPPA doctor $TMPDIR/doctor-bad.kap" \ + 'license is not specified' + +cat > "$TMPDIR/doctor-badcfg.kap" << 'KAPEOF' +system {} +packages {} +services {} +boot { + kernel = "linux" + init = "fakenit" + root = "/dev/sda1" + bootloader = "lilo" +} +KAPEOF + +check_fail "doctor warns missing hostname" \ + "$KAPPA doctor $TMPDIR/doctor-badcfg.kap" \ + 'hostname is not set' + +check_fail "doctor warns unknown init" \ + "$KAPPA doctor $TMPDIR/doctor-badcfg.kap" \ + 'not a recognized init system' + +check_fail "doctor warns unknown bootloader" \ + "$KAPPA doctor $TMPDIR/doctor-badcfg.kap" \ + 'not a recognized bootloader' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Repos: parsing and formatting ===" +echo "" + +cat > "$TMPDIR/repos-config.kap" << 'KAPEOF' +system { hostname = "test" } +repos { + upstream { + url = "https://packages.example.com/" + channels = ["stable", "testing"] + mirrors = [ + "https://cdn.example.com/", + "https://eu.example.com/", + ] + priority = 100 + } + local { + url = "file:///usr/local/kappa/repo/" + channels = ["default"] + } +} +packages { testpkg {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "parse-config with repos" \ + "$KAPPA parse-config $TMPDIR/repos-config.kap" \ + 'repos: 2' + +check "format repos roundtrip" \ + "$KAPPA format $TMPDIR/repos-config.kap" \ + 'upstream {' + +check "format repos preserves channels" \ + "$KAPPA format $TMPDIR/repos-config.kap" \ + 'channels = \[' + +check "format repos preserves mirrors" \ + "$KAPPA format $TMPDIR/repos-config.kap" \ + 'cdn.example.com' + +check "format repos preserves priority" \ + "$KAPPA format $TMPDIR/repos-config.kap" \ + 'priority = 100' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Backward compat: remotes flat list ===" +echo "" + +cat > "$TMPDIR/remotes-config.kap" << 'KAPEOF' +remotes = [ + "https://old.example.com/", + "https://old2.example.com/", +] +system { hostname = "test" } +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "flat remotes still parse" \ + "$KAPPA parse-config $TMPDIR/remotes-config.kap" \ + 'valid' + +check "format remotes roundtrip" \ + "$KAPPA format $TMPDIR/remotes-config.kap" \ + 'remotes = \[' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Index: generation and validation ===" +echo "" + +check "kappa index generates index.kap" \ + "$KAPPA index $TMPDIR" \ + 'packages indexed' + +check "validate index.kap" \ + "$KAPPA validate $TMPDIR/index.kap" \ + 'valid index' + +check "format index roundtrip" \ + "$KAPPA format $TMPDIR/index.kap" \ + 'index "' + +check_fail "index needs directory arg" \ + "$KAPPA index" \ + 'no directory' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Package operations: add and remove ===" +echo "" + +rm -f "$KAPPA_ROOT/system/config.kap" + +check "add creates config" \ + "$KAPPA add newpkg" \ + 'added newpkg' + +check "add with version" \ + "$KAPPA add verpkg 2.0" \ + 'added verpkg 2.0' + +check "add existing updates version" \ + "$KAPPA add newpkg 3.0" \ + 'updated newpkg' + +check "add existing idempotent" \ + "$KAPPA add verpkg" \ + 'already in packages' + +check "remove package" \ + "$KAPPA remove verpkg" \ + 'removed verpkg' + +check "remove nonexistent idempotent" \ + "$KAPPA remove nonexistent" \ + 'not in packages' + +check_fail "add needs name" \ + "$KAPPA add" \ + 'no package name' + +check_fail "remove needs name" \ + "$KAPPA remove" \ + 'no package name' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Install: list and rollback ===" +echo "" + +check "list empty" \ + "$KAPPA list" \ + 'no packages installed' + +mkdir -p "$KAPPA_ROOT/db" +cat > "$KAPPA_ROOT/db/installed" << 'EOF' +testpkg 1.0 a1b2c3d4e5f6a7b8 +otherpkg 2.0 b2c3d4e5f6a7b8c9 libother.so +EOF + +check "list shows installed" \ + "$KAPPA list" \ + '2 packages installed' + +check "list shows provides" \ + "$KAPPA list" \ + 'libother.so' + +mkdir -p "$KAPPA_ROOT/db/generations" +echo "a1b2c3d4e5f6a7b8" > "$KAPPA_ROOT/db/generations/gen-0001" +echo "b2c3d4e5f6a7b8c9" > "$KAPPA_ROOT/db/generations/gen-0002" + +check "rollback shows generations" \ + "$KAPPA rollback" \ + '2 generations' + +check "rollback shows gen IDs" \ + "$KAPPA rollback" \ + 'gen-0001' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Resolver: conflicts ===" +echo "" + +mkdir -p "$KAPPA_ROOT/cache/packages" + +cat > "$KAPPA_ROOT/cache/packages/a.kap" << 'KAPEOF' +package "a" { + version = "1.0" + source = "a.tar.gz" + conflicts = ["b"] + build { echo a } + install { echo a > ${destdir}/tmp/a } +} +KAPEOF +cat > "$KAPPA_ROOT/cache/packages/b.kap" << 'KAPEOF' +package "b" { + version = "1.0" + source = "b.tar.gz" + conflicts = ["a"] + build { echo b } + install { echo b > ${destdir}/tmp/b } +} +KAPEOF + +cat > "$TMPDIR/conflict-config.kap" << 'KAPEOF' +system { hostname = "test" } +packages { a {} b {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check_fail "resolve catches conflicts" \ + "$KAPPA resolve $TMPDIR/conflict-config.kap" \ + 'conflicts detected' + +check_fail "rebuild catches conflicts" \ + "$KAPPA rebuild $TMPDIR/conflict-config.kap" \ + 'conflicts detected' + +cat > "$TMPDIR/missing-config.kap" << 'KAPEOF' +system { hostname = "test" } +packages { nonexistent {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "resolve warns on missing" \ + "$KAPPA resolve $TMPDIR/missing-config.kap 2>&1" \ + 'not found in registry' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Scheduler: deptree ordering ===" +echo "" + +cat > "$KAPPA_ROOT/cache/packages/dep-a.kap" << 'KAPEOF' +package "dep-a" { + version = "1.0" + source = "a.tar.gz" + build { echo a } + install { echo a > ${destdir}/tmp/a } +} +KAPEOF +cat > "$KAPPA_ROOT/cache/packages/dep-b.kap" << 'KAPEOF' +package "dep-b" { + version = "1.0" + source = "b.tar.gz" + depends = [{ name = "dep-a" }] + build { echo b } + install { echo b > ${destdir}/tmp/b } +} +KAPEOF +cat > "$KAPPA_ROOT/cache/packages/dep-c.kap" << 'KAPEOF' +package "dep-c" { + version = "1.0" + source = "c.tar.gz" + depends = [{ name = "dep-b" }] + build { echo c } + install { echo c > ${destdir}/tmp/c } +} +KAPEOF + +cat > "$TMPDIR/deptree-config.kap" << 'KAPEOF' +system { hostname = "test" } +packages { dep-a {} dep-b {} dep-c {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "resolve deptree (a=0 deps)" \ + "$KAPPA resolve $TMPDIR/deptree-config.kap 2>&1" \ + 'dep-a (0 deps)' + +check "resolve deptree (c has dep)" \ + "$KAPPA resolve $TMPDIR/deptree-config.kap 2>&1" \ + 'dep-c (1 deps)' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Build: simple execution ===" +echo "" + +cat > "$TMPDIR/simple-build.kap" << 'KAPEOF' +package "simple" { + version = "1.0" + source = "simple-1.0.tar.gz" + sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + license = "MIT" + build { echo built } + check { echo checked } + install { + mkdir -p ${destdir}/tmp + echo done > ${destdir}/tmp/simple + } +} +KAPEOF + +check "build simple package" \ + "$KAPPA build $TMPDIR/simple-build.kap" \ + 'build successful' + +check "installed after build" \ + "$KAPPA list" \ + 'simple' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Build: error handling ===" +echo "" + +cat > "$TMPDIR/fail-build.kap" << 'KAPEOF' +package "failpkg" { + version = "1.0" + source = "fail.tar.gz" + sha256 = "abc" + license = "MIT" + build { exit 42 } + install { echo ok } +} +KAPEOF + +check_fail "build reports exit code" \ + "$KAPPA build $TMPDIR/fail-build.kap" \ + 'exited with code 42' + +check_fail "build reports failed phase" \ + "$KAPPA build $TMPDIR/fail-build.kap" \ + 'build' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Build: env operators ===" +echo "" + +cat > "$TMPDIR/env-ops.kap" << 'KAPEOF' +package "envops" { + version = "1.0" + source = "envops.tar.gz" + sha256 = "abc" + license = "MIT" + env { + FLAGS = "base" + FLAGS += "extra" + FLAGS ?= "soft" + } + build { echo ok } + install { mkdir -p ${destdir}/tmp } +} +KAPEOF + +check "format shows env =" \ + "$KAPPA format $TMPDIR/env-ops.kap" \ + 'FLAGS = "base"' + +check "format shows env +=" \ + "$KAPPA format $TMPDIR/env-ops.kap" \ + 'FLAGS += "extra"' + +check "format shows env ?=" \ + "$KAPPA format $TMPDIR/env-ops.kap" \ + 'FLAGS ?= "soft"' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Init systems: all 5 recognized ===" +echo "" + +for init in systemd openrc s6 runit dinit; do + cat > "$TMPDIR/init-$init.kap" << KAPEOF +system { hostname = "test" } +packages {} +services {} +boot { + kernel = "linux" + init = "$init" + root = "/dev/sda1" + bootloader = "limine" +} +users { + root { shell = "/bin/sh" } +} +KAPEOF + check "parse-config recognizes $init" \ + "$KAPPA parse-config $TMPDIR/init-$init.kap" \ + 'valid' +done + +# ═══════════════════════════════════════════ +echo "" +echo "=== Bootloaders: both recognized ===" +echo "" + +for bl in grub limine; do + cat > "$TMPDIR/bl-$bl.kap" << KAPEOF +system { hostname = "test" } +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "$bl" +} +users { + root { shell = "/bin/sh" } +} +KAPEOF + check "parse-config recognizes $bl" \ + "$KAPPA parse-config $TMPDIR/bl-$bl.kap" \ + 'valid' +done + +# ═══════════════════════════════════════════ +echo "" +echo "=== Variable interpolation: format preserves ===" +echo "" + +cat > "$TMPDIR/vars.kap" << 'KAPEOF' +package "vars" { + version = "1.0" + source = "https://example.com/vars-1.0.tar.gz" + sha256 = "abc" + license = "MIT" + build { + ./configure --prefix=${prefix} + make -j${jobs} + } + install { + make DESTDIR=${destdir} install + } +} +KAPEOF + +check "format preserves prefix var" \ + "$KAPPA format $TMPDIR/vars.kap" \ + 'prefix=${prefix}' + +check "format preserves jobs var" \ + "$KAPPA format $TMPDIR/vars.kap" \ + 'j${jobs}' + +check "format preserves destdir var" \ + "$KAPPA format $TMPDIR/vars.kap" \ + 'DESTDIR=${destdir}' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Paths: --root override ===" +echo "" + +ALTROOT="$TMPDIR/altroot" +mkdir -p "$ALTROOT/system" + +cat > "$ALTROOT/system/config.kap" << 'KAPEOF' +system { hostname = "alt" } +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "--root flag works" \ + "$KAPPA --root $ALTROOT parse-config $ALTROOT/system/config.kap" \ + 'valid' + +$KAPPA --root "$ALTROOT" add rootpkg 2>/dev/null +check "verify add wrote to alt root" \ + "grep -c rootpkg $ALTROOT/system/config.kap" \ + '1' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: rollback config ===" +echo "" + +cat > "$TMPDIR/rollback-cfg.kap" << 'KAPEOF' +system { + hostname = "test" + rollback { + keep = 3 + } +} +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "format preserves rollback keep" \ + "$KAPPA format $TMPDIR/rollback-cfg.kap" \ + 'keep = 3' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Config: system env block ===" +echo "" + +cat > "$TMPDIR/sysenv-config.kap" << 'KAPEOF' +system { + hostname = "test" + env { + CFLAGS = "-O2" + CFLAGS += "-pipe" + } +} +packages {} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "parse-config with system env" \ + "$KAPPA parse-config $TMPDIR/sysenv-config.kap" \ + 'valid' + +# ═══════════════════════════════════════════ +echo "" +echo "=== DSL: feature flags ===" +echo "" + +cat > "$TMPDIR/features-config.kap" << 'KAPEOF' +system { + hostname = "test" + features { + ssl = true + wayland = false + } +} +packages { + testpkg { + features { + debug = true + } + } +} +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "parse-config with features" \ + "$KAPPA parse-config $TMPDIR/features-config.kap" \ + 'valid' + +# ═══════════════════════════════════════════ +echo "" +echo "=== Rebuild: dry-run detection ===" +echo "" + +# Package must be in registry + installed DB for rebuild +cp "$TMPDIR/basic.kap" "$KAPPA_ROOT/cache/packages/testpkg.kap" +mkdir -p "$KAPPA_ROOT/db" +echo "testpkg 1.0 0000000000000000" > "$KAPPA_ROOT/db/installed" + +cat > "$TMPDIR/rebuild-config.kap" << 'KAPEOF' +system { hostname = "test" } +packages { testpkg {} } +services {} +boot { + kernel = "linux" + init = "s6" + root = "/dev/sda1" + bootloader = "limine" +} +KAPEOF + +check "rebuild dry-run detects changes" \ + "$KAPPA rebuild --dry-run $TMPDIR/rebuild-config.kap 2>&1" \ + 'nothing to rebuild' + +check "resolve shows build plan" \ + "$KAPPA resolve $TMPDIR/rebuild-config.kap 2>&1" \ + 'packages in build order' + +# ═══════════════════════════════════════════ +echo "" +echo "==========================================" +echo " Results: $PASS passed, $FAIL failed" +echo "==========================================" + +[ "$FAIL" -eq 0 ] || exit 1