kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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