run.sh (23479B)
1 #!/bin/sh 2 # Hermetic self-distribution suite — exercises `kit update` over packages built 3 # the same way `make dist` does (kit pkg create), without any network or any 4 # dependency on `make dist`. Every install root lives under the sandbox, so the 5 # whole flow is offline-first and reproducible (SOURCE_DATE_EPOCH pinned). 6 # 7 # Type-K mode-P harness: shared assert verbs route through the unified counters 8 # over $work. SERIAL by design — the version layout under $KIT_HOME is mutated 9 # in place across cases, so do not parallelize. 10 # 11 # Run by: make test-selfdist (KIT=<binary> sh test/dist/run.sh) 12 13 set -u 14 15 script_dir=$(cd "$(dirname "$0")" && pwd) 16 repo_root=$(cd "$script_dir/../.." && pwd) 17 18 KIT="${KIT:-$repo_root/build/kit}" 19 if [ ! -x "$KIT" ]; then 20 echo "dist: kit binary not found at $KIT" >&2 21 exit 2 22 fi 23 24 SECKEY="$repo_root/test/dist/keys/nonrelease.key" 25 PUBKEY="$repo_root/test/dist/keys/nonrelease.pub" 26 27 work=$(mktemp -d "${TMPDIR:-/tmp}/kit-dist-test.XXXXXX") 28 trap 'rm -rf "$work"' EXIT 29 30 # HOME + KIT_HOME live inside the sandbox so nothing escapes $work and the host 31 # user's real ~/.local/share/kit is never touched. KIT_HOME is repointed per 32 # scenario via the kit_home() helper below; HOME stays put as a fallback root. 33 HOME="$work/home" 34 SOURCE_DATE_EPOCH=1 35 export HOME SOURCE_DATE_EPOCH 36 mkdir -p "$HOME" "$work/stage" "$work/pkg" 37 38 KIT_KIT_DIR="$repo_root/test/lib" 39 . "$repo_root/test/lib/kit_sh_kit.sh" 40 kit_report_init 41 42 # Official build configuration is a release gate: URL + production anchors are 43 # mandatory, and the checked-in non-release anchor is rejected by key id. 44 run_fail "release-config-missing-fails" make -n -C "$repo_root" RELEASE=1 bin 45 run_fail "release-config-test-anchor-fails" make -n -C "$repo_root" \ 46 RELEASE=1 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 47 KIT_RELEASE_PUBKEYS="$PUBKEY" bin 48 { 49 printf '%s\n' 'untrusted comment: renamed release-looking public key' 50 sed -n '2,$p' "$PUBKEY" 51 } > "$work/copied-nonrelease.pub" 52 run_fail "release-config-copied-test-anchor-fails" make -n -C "$repo_root" \ 53 RELEASE=1 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 54 KIT_RELEASE_PUBKEYS="$work/copied-nonrelease.pub" bin 55 run_ok "release-config-keygen" "$KIT" pkg keygen -o "$work/release-config" 56 run_ok "release-config-production-shape" make -n -C "$repo_root" RELEASE=1 \ 57 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 58 KIT_RELEASE_PUBKEYS="$work/release-config.pub" bin 59 60 # The native distribution target is an official RELEASE=1 artifact path. It 61 # must never invent a channel, trust anchor, or signing key. Tests may use the 62 # checked-in non-release pair only through the same explicit opt-in as the 63 # multi-target release harness. 64 run_fail "release-dist-sign-key-required" make -n -C "$repo_root" dist 65 run_fail "release-dist-config-required" make -C "$repo_root" dist \ 66 KIT_SIGN_KEY="$work/release-config.key" 67 run_fail "release-dist-test-key-rejected" make -C "$repo_root" dist \ 68 KIT_SIGN_KEY="$SECKEY" \ 69 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 70 KIT_RELEASE_PUBKEYS="$PUBKEY" 71 { 72 printf '%s\n' 'untrusted comment: renamed release-looking secret key' 73 sed -n '2,$p' "$SECKEY" 74 } > "$work/copied-nonrelease.key" 75 run_fail "release-dist-copied-test-key-rejected" make -n -C "$repo_root" dist \ 76 KIT_SIGN_KEY="$work/copied-nonrelease.key" \ 77 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 78 KIT_RELEASE_PUBKEYS="$work/release-config.pub" 79 # Passwordless Minisign ignores its salt field. Mutating that field produces a 80 # different base64 record for the same valid signer identity; the release gate 81 # must still recognize it rather than relying on raw record equality. 82 { 83 printf '%s\n' 'untrusted comment: reserialized release-looking secret key' 84 sed -n '2p' "$SECKEY" | awk '{ print substr($0, 1, 12) "B" substr($0, 14) }' 85 } > "$work/reserialized-nonrelease.key" 86 printf 'identity regression payload\n' > "$work/identity-payload" 87 run_ok "release-reserialized-test-key-still-signs" "$KIT" pkg sign \ 88 -s "$work/reserialized-nonrelease.key" "$work/identity-payload" 89 run_fail "release-dist-reserialized-test-key-rejected" make -n \ 90 -C "$repo_root" dist KIT_SIGN_KEY="$work/reserialized-nonrelease.key" \ 91 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 92 KIT_RELEASE_PUBKEYS="$work/release-config.pub" 93 run_fail "release-script-copied-test-key-rejected" env \ 94 KIT_SIGN_KEY="$work/copied-nonrelease.key" \ 95 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 96 KIT_RELEASE_PUBKEYS="$work/release-config.pub" \ 97 KIT_RELEASE_ALLOW_DIRTY=1 KIT_RELEASE_TARGETS=macos-aa64 \ 98 "$repo_root/scripts/release.sh" 99 run_ok "release-dist-explicit-test-shape" make -n -C "$repo_root" dist \ 100 KIT_SIGN_KEY="$SECKEY" KIT_RELEASE_ALLOW_TEST_KEY=1 \ 101 KIT_UPDATE_INDEX_URL=https://updates.example/stable.index \ 102 KIT_RELEASE_PUBKEYS="$PUBKEY" 103 run_ok "release-dist-dev-shape" make -n -C "$repo_root" dist-dev 104 105 # ---- local helpers --------------------------------------------------------- 106 107 have_cmd() { command -v "$1" >/dev/null 2>&1; } 108 109 # kit_home DIR : point every subsequent `kit update` at this install root. 110 kh= 111 kit_home() { kh="$work/$1"; export KIT_HOME="$kh"; } 112 113 # run_usage NAME CMD... : require the driver's reserved usage status (2). 114 run_usage() { 115 name=$1 116 shift 117 "$@" > "$work/$name.out" 2> "$work/$name.err" 118 got=$? 119 if [ "$got" -eq 2 ]; then 120 ok "$name" 121 else 122 { 123 printf 'expected usage status 2, got %s\n' "$got" 124 sed 's/^/stdout: /' "$work/$name.out" 125 sed 's/^/stderr: /' "$work/$name.err" 126 } > "$work/$name.diag" 127 not_ok "$name" "$work/$name.diag" 128 fi 129 } 130 131 # stage_kpkg VER : build a signed fat kit kpkg containing bin/kit (= $KIT), a 132 # VERSION file, and a small lib/ payload. Records a pass/fail through run_ok and 133 # leaves the kpkg path in $LAST_KPKG (NOT echoed — calling this in a command 134 # substitution would swallow the run_ok PASS line into the captured value). 135 LAST_KPKG= 136 stage_kpkg() { 137 ver=$1 138 sdir="$work/stage/$ver" 139 LAST_KPKG="$work/pkg/kit-$ver.kpkg" 140 mkdir -p "$sdir/bin" "$sdir/lib" 141 cp "$KIT" "$sdir/bin/kit" 142 printf '%s\n' "$ver" > "$sdir/VERSION" 143 printf 'self-distribution payload for %s\n' "$ver" > "$sdir/lib/data.txt" 144 run_ok "create-$ver" "$KIT" pkg create \ 145 --name kit --version "$ver" --format kpkg --native-shape fat \ 146 -s "$SECKEY" --root "$sdir" -o "$LAST_KPKG" 147 } 148 149 # current_is NAME KH_DIR WANT : assert $KH_DIR/current resolves to versions/WANT. 150 current_is() { 151 name=$1; kdir=$2; want=$3 152 got=$(readlink "$kdir/current" 2>/dev/null) 153 if [ "$got" = "versions/$want" ]; then ok "$name" 154 else 155 echo "current -> '$got', want 'versions/$want'" > "$work/$name.diag" 156 not_ok "$name" "$work/$name.diag" 157 fi 158 } 159 160 # runs_kit NAME EXE [WANTVER] : run EXE --version; assert it executes and prints 161 # a "kit ..." banner. WANTVER (optional) additionally requires that string. (All 162 # staged packages bundle the SAME test binary, so per-version banner text is 163 # only asserted where that binary's own version is known to match.) 164 runs_kit() { 165 name=$1; exe=$2; wantver=${3:-kit } 166 if "$exe" --version > "$work/$name.out" 2>&1 && 167 grep -F "$wantver" "$work/$name.out" >/dev/null 2>&1; then 168 ok "$name" 169 else 170 not_ok "$name" "$work/$name.out" 171 fi 172 } 173 174 flip_byte() { 175 file=$1; off=$2 176 printf '\377' | dd of="$file" bs=1 seek="$off" count=1 conv=notrunc >/dev/null 2>&1 177 } 178 179 # host_triple : 3rd field of `kit <ver> (<build>, <TRIPLE>)`. 180 host_triple() { 181 "$KIT" --version 2>/dev/null | sed -n 's/^kit [^ ]* (.*, \(.*\))/\1/p' | sed -n '1p' 182 } 183 184 # An unauthenticated/nonexistent check is an operational failure and a clean 185 # read-only invocation must not create KIT_HOME. 186 kit_home kh-check-no-config 187 unset KIT_UPDATE_INDEX_URL 188 run_fail "check-no-config-fails" "$KIT" update --check 189 contains "check-no-config-diagnostic" "$work/check-no-config-fails.err" \ 190 "no channel index configured" 191 if [ -e "$KIT_HOME" ]; then 192 echo "--check without configuration created KIT_HOME" > \ 193 "$work/check-no-config-no-state.diag" 194 not_ok "check-no-config-no-state" "$work/check-no-config-no-state.diag" 195 else 196 ok "check-no-config-no-state" 197 fi 198 kit_home kh-check-fetch-failure 199 run_fail "check-fetch-failure" "$KIT" update --check \ 200 --index "file://$work/does-not-exist.index" 201 if [ -e "$KIT_HOME" ]; then 202 echo "failed-fetch --check created KIT_HOME" > \ 203 "$work/check-fetch-failure-no-state.diag" 204 not_ok "check-fetch-failure-no-state" \ 205 "$work/check-fetch-failure-no-state.diag" 206 else 207 ok "check-fetch-failure-no-state" 208 fi 209 210 # Remote update transport is intentionally curl-only. With curl absent, Kit 211 # must fail instead of silently changing downloader/TLS behavior to wget. 212 mkdir -p "$work/fetch-path" 213 printf '%s\n' '#!/bin/sh' 'printf called > "$WGET_MARKER"' 'exit 0' > \ 214 "$work/fetch-path/wget" 215 chmod +x "$work/fetch-path/wget" 216 kit_home kh-check-no-curl 217 saved_path=$PATH 218 WGET_MARKER="$work/wget-called" PATH="$work/fetch-path" \ 219 run_fail "check-curl-only" "$KIT" update --check \ 220 --index https://updates.invalid/stable.index 221 PATH=$saved_path 222 export PATH 223 if [ -e "$work/wget-called" ]; then 224 echo "update transport invoked wget when curl was unavailable" > \ 225 "$work/check-curl-only.diag" 226 not_ok "check-no-wget-fallback" "$work/check-curl-only.diag" 227 else 228 ok "check-no-wget-fallback" 229 fi 230 231 # =========================================================================== # 232 # (a) offline install of 2026.6.0 233 # =========================================================================== # 234 kit_home kh 235 stage_kpkg 2026.6.0 236 kpkg_60="$LAST_KPKG" 237 238 # --check is a channel-index query, never an alias for a local mutating action. 239 kit_home kh-check-local-package 240 run_usage "check-local-package-usage" "$KIT" update --check "$kpkg_60" 241 if [ -e "$KIT_HOME" ]; then 242 echo "local-package --check created KIT_HOME" > \ 243 "$work/check-local-package-no-state.diag" 244 not_ok "check-local-package-no-state" \ 245 "$work/check-local-package-no-state.diag" 246 else 247 ok "check-local-package-no-state" 248 fi 249 kit_home kh 250 251 run_ok "a-install-60" "$KIT" update "$kpkg_60" 252 contains "a-install-60-msg" "$work/a-install-60.out" "installed kit 2026.6.0" 253 current_is "a-current-60" "$kh" 2026.6.0 254 assert_file_exists "a-versioned-bin" "$kh/versions/2026.6.0/bin/kit" 255 assert_file_exists "a-link-kit" "$kh/bin/kit" 256 assert_file_exists "a-link-cc" "$kh/bin/cc" 257 is_executable "a-link-kit-exec" "$kh/bin/kit" 258 is_executable "a-link-cc-exec" "$kh/bin/cc" 259 runs_kit "a-link-kit-runs" "$kh/bin/kit" "kit 2026.6.0" 260 contains "a-version-file" "$kh/versions/2026.6.0/VERSION" "2026.6.0" 261 262 # =========================================================================== # 263 # (b) second install + atomic flip; --list marks the new current 264 # =========================================================================== # 265 stage_kpkg 2026.6.1 266 kpkg_61="$LAST_KPKG" 267 run_ok "b-install-61" "$KIT" update "$kpkg_61" 268 current_is "b-current-61" "$kh" 2026.6.1 269 runs_kit "b-link-kit-runs-61" "$kh/bin/kit" 270 contains "b-version-file-61" "$kh/versions/2026.6.1/VERSION" "2026.6.1" 271 run_ok "b-list" "$KIT" update --list 272 contains "b-list-has-60" "$work/b-list.out" "2026.6.0" 273 contains "b-list-star-61" "$work/b-list.out" "* 2026.6.1" 274 275 # =========================================================================== # 276 # (c) offline --version flip back to 2026.6.0 (no network) 277 # =========================================================================== # 278 run_ok "c-flip-60" "$KIT" update --version 2026.6.0 279 contains "c-flip-60-msg" "$work/c-flip-60.out" "kit 2026.6.0 is now current" 280 current_is "c-current-60" "$kh" 2026.6.0 281 282 # =========================================================================== # 283 # (d) --rollback from a known current to the previous version 284 # =========================================================================== # 285 # Make 2026.6.1 current first, then roll back to 2026.6.0 (the prior CalVer). 286 run_ok "d-prep-flip-61" "$KIT" update --version 2026.6.1 287 current_is "d-prep-current-61" "$kh" 2026.6.1 288 run_ok "d-rollback" "$KIT" update --rollback 289 current_is "d-current-60" "$kh" 2026.6.0 290 contains "d-rollback-warn" "$work/d-rollback.out" "re-upgrade" 291 292 # =========================================================================== # 293 # (e) --prune removes every non-current version 294 # =========================================================================== # 295 # current is 2026.6.0; prune must drop versions/2026.6.1. 296 run_ok "e-prune" "$KIT" update --prune 297 contains "e-prune-msg" "$work/e-prune.out" "removed kit 2026.6.1" 298 if [ -e "$kh/versions/2026.6.1" ]; then 299 echo "versions/2026.6.1 still present after prune" > "$work/e-pruned-gone.diag" 300 not_ok "e-pruned-gone" "$work/e-pruned-gone.diag" 301 else 302 ok "e-pruned-gone" 303 fi 304 run_ok "e-list-after" "$KIT" update --list 305 contains "e-list-only-60" "$work/e-list-after.out" "* 2026.6.0" 306 if grep -F "2026.6.1" "$work/e-list-after.out" >/dev/null 2>&1; then 307 echo "2026.6.1 still listed after prune" > "$work/e-list-no-61.diag" 308 not_ok "e-list-no-61" "$work/e-list-no-61.diag" 309 else 310 ok "e-list-no-61" 311 fi 312 313 # =========================================================================== # 314 # (f) explicit-key verify paths (pkg verify + update --key) 315 # =========================================================================== # 316 run_ok "f-verify-pubkey" "$KIT" pkg verify -p "$PUBKEY" "$kpkg_60" 317 contains "f-verify-name" "$work/f-verify-pubkey.out" "ok: kit 2026.6.0" 318 kit_home kh-f 319 run_ok "f-update-key" "$KIT" update --key "$PUBKEY" "$kpkg_60" 320 current_is "f-current-60" "$work/kh-f" 2026.6.0 321 322 # =========================================================================== # 323 # (g) wrong key is rejected and leaves current untouched 324 # =========================================================================== # 325 # Fresh root with 2026.6.0 already current, then try installing 2026.6.1 with a 326 # foreign key — must fail and keep current at 2026.6.0. 327 kit_home kh-g 328 run_ok "g-seed-60" "$KIT" update "$kpkg_60" 329 current_is "g-seed-current-60" "$work/kh-g" 2026.6.0 330 run_ok "g-keygen-other" "$KIT" pkg keygen -o "$work/other" 331 run_fail "g-update-wrong-key" "$KIT" update --key "$work/other.pub" "$kpkg_61" 332 current_is "g-current-unchanged" "$work/kh-g" 2026.6.0 333 334 # =========================================================================== # 335 # (h) tamper is rejected and leaves current untouched 336 # =========================================================================== # 337 # Reuse kh-g (current = 2026.6.0). Flip one byte well inside the kpkg. 338 cp "$kpkg_61" "$work/pkg/tampered.kpkg" 339 tsize=$(wc -c < "$work/pkg/tampered.kpkg") 340 flip_byte "$work/pkg/tampered.kpkg" $((tsize / 2)) 341 run_fail "h-update-tampered" "$KIT" update "$work/pkg/tampered.kpkg" 342 current_is "h-current-unchanged" "$work/kh-g" 2026.6.0 343 344 # =========================================================================== # 345 # (i) bootstrap: stock minisign verifies a kit-signed tar.gz 346 # =========================================================================== # 347 run_ok "i-create-targz" "$KIT" pkg create \ 348 --name kit --version 2026.6.0 --format tar.gz --native-shape fat \ 349 -s "$SECKEY" --root "$work/stage/2026.6.0" -o "$work/pkg/kit-boot.tar.gz" 350 run_ok "i-sign-targz" "$KIT" pkg sign -s "$SECKEY" "$work/pkg/kit-boot.tar.gz" 351 assert_file_exists "i-sig-written" "$work/pkg/kit-boot.tar.gz.minisig" 352 if have_cmd minisign; then 353 run_ok "i-minisign-verify" minisign -Vm "$work/pkg/kit-boot.tar.gz" -p "$PUBKEY" 354 else 355 skip_test "i-minisign-verify" "minisign not installed" 356 fi 357 358 # =========================================================================== # 359 # (j) networked + monotonic via file:// channel index 360 # =========================================================================== # 361 # Both the channel index and selected package are signed by the same identity; 362 # URLs remain fetch hints. We hand-write a `kit-release 1` index (format per 363 # test/api/release_index_test.c) pointing at the local 2026.6.0 kpkg over a 364 # file:// URL. If the fetch transport is unavailable here, the whole block is 365 # skipped rather than failed. 366 if ! have_cmd curl; then 367 skip_test "j-network" "curl not available for file:// fetch" 368 else 369 triple=$(host_triple) 370 if [ -z "$triple" ]; then 371 skip_test "j-network" "could not determine host triple" 372 else 373 # Re-derive the 2026.6.0 kpkg id from its create output. 374 id60=$(first_hex_id "$work/create-2026.6.0.out") 375 idx="$work/channel-60.index" 376 { 377 printf 'kit-release 1\n' 378 printf 'channel = stable\n' 379 printf 'version = 2026.6.0\n' 380 printf 'hash = blake2b-256\n' 381 printf '\n' 382 printf '[host]\n' 383 printf 'target = %s\n' "$triple" 384 printf 'kpkg = %s\n' "$id60" 385 printf 'url = file://%s\n' "$kpkg_60" 386 } > "$idx" 387 run_ok "j-sign-index" "$KIT" pkg sign -s "$SECKEY" \ 388 --comment "selfdist stable index" "$idx" 389 390 sed 's/version = 2026\.6\.0/version = 2025.1.0/' "$idx" > \ 391 "$work/channel-older.index" 392 run_ok "j-sign-older-index" "$KIT" pkg sign -s "$SECKEY" \ 393 "$work/channel-older.index" 394 kit_home kh-j-running-floor 395 run_fail "j-running-version-is-floor" "$KIT" update \ 396 --index "file://$work/channel-older.index" 397 contains "j-running-version-floor-message" \ 398 "$work/j-running-version-is-floor.err" "older than running kit" 399 400 sed 's/version = 2026\.6\.0/version = 2026.6.1/' "$idx" > \ 401 "$work/channel-version-mismatch.index" 402 run_ok "j-sign-version-mismatch-index" "$KIT" pkg sign -s "$SECKEY" \ 403 "$work/channel-version-mismatch.index" 404 kit_home kh-j-version-mismatch 405 run_fail "j-package-version-mismatch-fails" "$KIT" update \ 406 --index "file://$work/channel-version-mismatch.index" 407 contains "j-package-version-mismatch-message" \ 408 "$work/j-package-version-mismatch-fails.err" \ 409 "does not match expected" 410 411 zero_id=0000000000000000000000000000000000000000000000000000000000000000 412 sed "s/kpkg = $id60/kpkg = $zero_id/" "$idx" > \ 413 "$work/channel-id-mismatch.index" 414 run_ok "j-sign-id-mismatch-index" "$KIT" pkg sign -s "$SECKEY" \ 415 "$work/channel-id-mismatch.index" 416 kit_home kh-j-id-mismatch 417 run_fail "j-package-id-mismatch-fails" "$KIT" update \ 418 --index "file://$work/channel-id-mismatch.index" 419 contains "j-package-id-mismatch-message" \ 420 "$work/j-package-id-mismatch-fails.err" \ 421 "package id does not match" 422 423 cp "$idx" "$work/channel-other-signer.index" 424 run_ok "j-sign-index-other-identity" "$KIT" pkg sign \ 425 -s "$work/other.key" "$work/channel-other-signer.index" 426 kit_home kh-j-other-signer 427 run_fail "j-key-applies-to-index-and-package" "$KIT" update \ 428 --key "$work/other.pub" \ 429 --index "file://$work/channel-other-signer.index" 430 431 cp "$idx" "$work/channel-tampered.index" 432 cp "$idx.minisig" "$work/channel-tampered.index.minisig" 433 printf '# changed after signing\n' >> "$work/channel-tampered.index" 434 kit_home kh-j-bad-signature 435 run_fail "j-tampered-index-fails" "$KIT" update --check \ 436 --index "file://$work/channel-tampered.index" 437 if [ -e "$KIT_HOME" ]; then 438 echo "bad-index --check created KIT_HOME" > \ 439 "$work/j-tampered-index-no-state.diag" 440 not_ok "j-tampered-index-no-state" \ 441 "$work/j-tampered-index-no-state.diag" 442 else 443 ok "j-tampered-index-no-state" 444 fi 445 446 printf 'not a release index\n' > "$work/channel-malformed.index" 447 run_ok "j-sign-malformed-index" "$KIT" pkg sign -s "$SECKEY" \ 448 "$work/channel-malformed.index" 449 kit_home kh-j-malformed 450 run_fail "j-authenticated-malformed-index-fails" "$KIT" update --check \ 451 --index "file://$work/channel-malformed.index" 452 453 # Probe the file:// transport against a fresh root before asserting: 454 # if it cannot fetch+install here, skip the block gracefully. 455 kit_home kh-j-probe 456 if "$KIT" update --index "file://$idx" > "$work/j-probe.out" 2>&1; then 457 current_is "j-probe-install-60" "$work/kh-j-probe" 2026.6.0 458 459 # --check reports the latest channel version, changing nothing. 460 kit_home kh-j-check 461 run_ok "j-check" "$KIT" update --index "file://$idx" --check 462 contains "j-check-latest" "$work/j-check.out" "latest 2026.6.0" 463 contains "j-check-none" "$work/j-check.out" "installed current: (none)" 464 if [ -e "$work/kh-j-check/current" ]; then 465 echo "--check created a current pointer" > "$work/j-check-nochange.diag" 466 not_ok "j-check-nochange" "$work/j-check-nochange.diag" 467 else 468 ok "j-check-nochange" 469 fi 470 if [ -e "$work/kh-j-check" ]; then 471 echo "--check left persistent KIT_HOME state" > \ 472 "$work/j-check-no-persistent-state.diag" 473 not_ok "j-check-no-persistent-state" \ 474 "$work/j-check-no-persistent-state.diag" 475 else 476 ok "j-check-no-persistent-state" 477 fi 478 479 kit_home kh-j-dry-run 480 run_ok "j-dry-run-authenticates-package" "$KIT" update \ 481 --index "file://$idx" --dry-run 482 contains "j-dry-run-plan" "$work/j-dry-run-authenticates-package.out" \ 483 "would install kit 2026.6.0" 484 if [ -e "$work/kh-j-dry-run" ]; then 485 echo "--dry-run left persistent KIT_HOME state" > \ 486 "$work/j-dry-run-no-state.diag" 487 not_ok "j-dry-run-no-state" "$work/j-dry-run-no-state.diag" 488 else 489 ok "j-dry-run-no-state" 490 fi 491 492 # Fresh root: networked fetch + install 2026.6.0. 493 kit_home kh-j-fetch 494 run_ok "j-fetch-install" "$KIT" update --index "file://$idx" 495 current_is "j-fetch-current-60" "$work/kh-j-fetch" 2026.6.0 496 run_ok "j-check-no-update" "$KIT" update --index "file://$idx" --check 497 contains "j-check-no-update-current" "$work/j-check-no-update.out" \ 498 "installed current: 2026.6.0" 499 500 # Monotonic guard: with current = 2026.6.1, an index advertising the 501 # older 2026.6.0 is refused, but --allow-downgrade succeeds. 502 run_ok "j-fetch-up-to-61" "$KIT" update "$kpkg_61" 503 current_is "j-fetch-current-61" "$work/kh-j-fetch" 2026.6.1 504 run_fail "j-monotonic-reject" "$KIT" update --index "file://$idx" 505 contains "j-monotonic-msg" "$work/j-monotonic-reject.err" "older than installed" 506 current_is "j-monotonic-unchanged" "$work/kh-j-fetch" 2026.6.1 507 run_ok "j-allow-downgrade" "$KIT" update --index "file://$idx" --allow-downgrade 508 current_is "j-downgrade-current-60" "$work/kh-j-fetch" 2026.6.0 509 else 510 skip_test "j-network" "file:// fetch not supported in this environment" 511 fi 512 fi 513 fi 514 515 kit_summary dist 516 kit_exit