#!/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' # --features and --config flags check "add with --features" \ "$KAPPA add featpkg --features 'ssl=on,gzip=off'" \ 'added featpkg' check "add features present in config" \ "grep -c 'ssl = true' $KAPPA_ROOT/system/config.kap" \ '1' check "update existing with --features" \ "$KAPPA add featpkg --features 'http2=on'" \ 'updated featpkg' check "add with --config" \ "$KAPPA add cfgpkg --config 'port=9090,worker=4'" \ 'added cfgpkg' check "config values written to file" \ "grep 'port = 9090' $KAPPA_ROOT/system/config.kap" \ 'port = 9090' check "add with features and config together" \ "$KAPPA add combopkg --features 'ssl=on' --config 'host=localhost'" \ 'added combopkg' check "features and config both in file" \ "grep -c ssl $KAPPA_ROOT/system/config.kap" \ '2' check "add idempotent with no flags" \ "$KAPPA add cfgpkg" \ 'already in packages' # ═══════════════════════════════════════════ 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)' # Transitive dependency resolution cat > "$KAPPA_ROOT/cache/packages/trans-a.kap" << 'TRANSEOF' package "trans-a" { version = "1.0" source = "a.tar.gz" sha256 = "abc" license = "MIT" build { echo a } install { echo a } } TRANSEOF cat > "$KAPPA_ROOT/cache/packages/trans-b.kap" << 'TRANSEOF' package "trans-b" { version = "1.0" source = "b.tar.gz" sha256 = "abc" license = "MIT" depends = [{ name = "trans-a" }] build { echo b } install { echo b } } TRANSEOF cat > "$KAPPA_ROOT/cache/packages/trans-c.kap" << 'TRANSEOF' package "trans-c" { version = "1.0" source = "c.tar.gz" sha256 = "abc" license = "MIT" depends = [{ name = "trans-b" }] build { echo c } install { echo c } } TRANSEOF cat > "$TMPDIR/transitive-config.kap" << 'TRANSEOF' system { hostname = "test" } packages { trans-c {} } services {} boot { kernel = "linux" init = "s6" root = "/dev/sda1" bootloader = "limine" } TRANSEOF check "transitive deps auto-resolve (only c declared)" \ "$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \ 'trans-a (0 deps)' check "transitive deps include intermediate" \ "$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \ 'trans-b (1 deps)' check "transitive deps resolve root last" \ "$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \ 'trans-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 "" echo "=== Search: cached index queries ===" echo "" mkdir -p "$KAPPA_ROOT/cache/indexes" cat > "$KAPPA_ROOT/cache/indexes/test-repo-stable.kap" << 'SRCHEOF' index "test-repo/stable" { trans-a { version = "1.0" } trans-b { version = "1.0" } trans-c { version = "1.0" } testpkg { version = "1.0" } } SRCHEOF check "search finds package by name" \ "$KAPPA search trans-a" \ 'trans-a' check "search finds by substring" \ "$KAPPA search ans" \ 'trans-a' check "search shows repo source" \ "$KAPPA search trans-a" \ 'test-repo/stable' check "search no match" \ "$KAPPA search zzz999" \ 'no packages matching' echo "" echo "=== Upgrade: version comparison ===" echo "" mkdir -p "$KAPPA_ROOT/db" cat > "$KAPPA_ROOT/db/installed" << 'UPGEOF' trans-a 0.9 a1b2c3d4e5f6a7b8 trans-b 0.9 b2c3d4e5f6a7b8c9 trans-c 1.0 c3d4e5f6a7b8c9d0 UPGEOF check "upgrade detects newer versions" \ "$KAPPA upgrade 2>&1" \ 'trans-a' check "upgrade shows old version" \ "$KAPPA upgrade 2>&1" \ '0.9' check "upgrade dry-run does not apply" \ "$KAPPA upgrade --dry-run 2>&1" \ 'run kappa add' echo "" echo "=== Bump: explicit version update ===" echo "" cat > "$TMPDIR/bump-test.kap" << 'BMPEOF' package "bumpme" { const version = "4.4.0" const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz" sha256 = "oldhash" license = "GPL-3.0-or-later" build { make } install { make install } } BMPEOF check "bump updates version" \ "$KAPPA bump $TMPDIR/bump-test.kap 4.4.1" \ 'bumped bumpme' check "bump writes correct version" \ "grep -c '4.4.1' $TMPDIR/bump-test.kap" \ '1' check "bump preserves source URL" \ "grep -c 'make-' $TMPDIR/bump-test.kap" \ '1' check_fail "bump requires version arg" \ "$KAPPA bump $TMPDIR/bump-test.kap" \ 'requires' echo "" echo "=== Bump-all: directory scan ===" echo "" BPALLDIR="$TMPDIR/bumpall-pkgs" mkdir -p "$BPALLDIR" cat > "$BPALLDIR/make.kap" << 'BMPEOF' package "make" { const version = "4.4.0" const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz" sha256 = "old" license = "GPL-3.0-or-later" build { make } install { make install } } BMPEOF check "bump-all finds newer version" \ "$KAPPA bump-all $BPALLDIR" \ '4.4.0 → 4.4.1' check "bump-all reports count" \ "$KAPPA bump-all $BPALLDIR" \ 'bumped' check_fail "bump-all needs directory" \ "$KAPPA bump-all" \ 'requires a directory' rm -f "$KAPPA_ROOT/db/installed" 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