Skip to content

Commit 28cb66d

Browse files
test(vm): vm-09 pearlite bootstrap idempotency (#59)
Closes the M3-rolled-forward `vm-09-nix-bootstrap` task per ADR-0012. Two-step scenario: 1. With nix already on PATH, run `pearlite bootstrap` → install: "already" (nix --version short-circuits), nix_conf_written: true (sandbox starts with no nix.conf; the experimental-features line gets written). 2. Re-run the same command → nix_conf_written: false (the line is already in the file from step 1, idempotent skip per ADR-0013). The Determinate-installer execution path is intentionally NOT exercised here: doing that on the runner would mutate /nix and is unsafe outside a disposable image. ADR-004 SHA verification is unit-tested in pearlite-userenv. vm-09 covers the operational path operators hit on healthy hosts. Gated behind PEARLITE_VM_TEST=1; expects nix and nickel-lang on PATH (VM-runner toolchain). README index updated. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 41b39a7 commit 28cb66d

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

tests/vm/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ runner is gated by ADR-0009 (CachyOS-fidelity CI runner choice).
2121
| `vm-06-failure-record.sh` | mutating | Induced `APPLY_SHA_MISMATCH` failure — asserts exit 4, forensic JSON at `<failures_dir>/<plan-id>.json`, and that `gen show` surfaces the SHA-256 message. |
2222
| `vm-07-user-env-apply.sh` | mutating | Per-user `home-manager switch` via `pearlite apply` (PRD §8.2 phase 7). Asserts `state.toml`'s `[[managed.user_env]]` records the user's `config_hash` after the switch. Requires `home-manager` on `PATH` and the `$PEARLITE_VM_USER` (default `pearlite-vm`) login present on the VM. |
2323
| `vm-08-user-env-drift.sh` | mutating | Drift detection — apply v1, idempotent re-plan emits no action, mutate `home.nix` to v2, re-plan emits `user_env_switch`, apply v2 updates `state.toml` `config_hash` and keeps `managed.user_env` at one row per user (upsert). |
24+
| `vm-09-nix-bootstrap.sh` | mutating | `pearlite bootstrap` (ADR-0012) idempotency — short-circuits when `nix --version` already succeeds, writes `/etc/nix/nix.conf` once, second run reports `nix_conf_written: false`. The actual Determinate-installer execution path is not exercised here; ADR-004 SHA verification is unit-tested in `crates/pearlite-userenv`. |
2425

2526
Mutating scripts refuse to run unless `PEARLITE_VM_TEST=1` is set in
2627
the environment — they install/remove packages, write to `/etc`, and

tests/vm/vm-09-nix-bootstrap.sh

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/bin/sh
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
# Copyright (C) 2026 Mohamed Hammad
4+
#
5+
# vm-09: pearlite bootstrap idempotency + nix.conf write (ADR-0012).
6+
#
7+
# Two steps:
8+
#
9+
# 1. With nix already on PATH, run `pearlite bootstrap` against a
10+
# sandboxed host file declaring nix.installer.expected_sha256.
11+
# Expect install: "already" (short-circuit via `nix --version`)
12+
# and nix_conf_written: true (the test sandbox starts without
13+
# a nix.conf).
14+
# 2. Re-run the same command. Expect install: "already" still, and
15+
# nix_conf_written: false — the experimental-features line is
16+
# already in the file from step 1, idempotent skip.
17+
#
18+
# This scenario does NOT exercise the actual Determinate installer
19+
# fetch + exec path: doing that on the runner would mutate /nix and
20+
# is not safe outside a disposable VM image. The SHA-verification
21+
# path (ADR-004) is covered by unit tests in
22+
# crates/pearlite-userenv/src/installer.rs.
23+
#
24+
# Mutating: requires PEARLITE_VM_TEST=1.
25+
#
26+
# POSIX sh; no Bash-isms.
27+
28+
set -eu
29+
30+
if [ "${PEARLITE_VM_TEST:-}" != "1" ]; then
31+
printf 'vm-09: refusing to run without PEARLITE_VM_TEST=1\n' >&2
32+
exit 2
33+
fi
34+
35+
if ! command -v nix >/dev/null 2>&1; then
36+
printf 'vm-09: this scenario expects nix to be installed on the VM image.\n' >&2
37+
printf ' (vm-09 exercises the short-circuit path; the actual install\n' >&2
38+
printf ' path would mutate /nix and is unit-tested elsewhere.)\n' >&2
39+
exit 2
40+
fi
41+
42+
PEARLITE_BIN="${PEARLITE_BIN:-}"
43+
sandbox=$(mktemp -d)
44+
trap 'rm -rf "$sandbox"' EXIT INT TERM
45+
46+
if [ -z "$PEARLITE_BIN" ]; then
47+
cargo build --quiet --release -p pearlite-cli
48+
PEARLITE_BIN="$(cargo metadata --format-version=1 --no-deps \
49+
| grep -o '"target_directory":"[^"]*"' \
50+
| head -1 \
51+
| cut -d'"' -f4)/release/pearlite"
52+
fi
53+
54+
mkdir -p "$sandbox/repo/hosts"
55+
56+
# A script the bootstrap command will hash. Content doesn't matter
57+
# for the short-circuit path (install_if_missing returns Already
58+
# before reading the script), but we declare the matching SHA so a
59+
# future test that exercises the install path could reuse this fixture.
60+
script="$sandbox/installer.sh"
61+
cat > "$script" <<'SCRIPT'
62+
#!/bin/sh
63+
# vm-09 stub Determinate installer — must NEVER execute in this scenario.
64+
exit 99
65+
SCRIPT
66+
chmod +x "$script"
67+
68+
script_sha=$(sha256sum "$script" | awk '{print $1}')
69+
70+
cat > "$sandbox/repo/hosts/vm-test.ncl" <<NCL
71+
{
72+
meta = { hostname = "vm-test", timezone = "UTC", arch_level = "v3", locale = "en_US.UTF-8", keymap = "us" },
73+
kernel = { package = "linux-cachyos" },
74+
nix = { installer = { expected_sha256 = "$script_sha" } },
75+
}
76+
NCL
77+
78+
cat > "$sandbox/state.toml" <<'TOML'
79+
schema_version = 1
80+
host = "vm-test"
81+
tool_version = "0.1.0"
82+
config_dir = "/tmp/repo"
83+
84+
[managed]
85+
pacman = []
86+
cargo = []
87+
88+
[adopted]
89+
pacman = []
90+
cargo = []
91+
TOML
92+
93+
nix_conf="$sandbox/nix.conf"
94+
95+
# Step 1: bootstrap on a clean sandbox (no nix.conf yet).
96+
"$PEARLITE_BIN" \
97+
--format=json \
98+
--config-dir="$sandbox/repo" \
99+
--state-file="$sandbox/state.toml" \
100+
bootstrap \
101+
--host-file="$sandbox/repo/hosts/vm-test.ncl" \
102+
--installer-script="$script" \
103+
--nix-conf="$nix_conf" \
104+
> "$sandbox/bootstrap-1.json" || {
105+
printf 'vm-09: step 1 bootstrap failed:\n' >&2
106+
cat "$sandbox/bootstrap-1.json" >&2
107+
exit 1
108+
}
109+
110+
if ! grep -q '"install":"already"' "$sandbox/bootstrap-1.json"; then
111+
printf 'vm-09: step 1 install did not short-circuit:\n' >&2
112+
cat "$sandbox/bootstrap-1.json" >&2
113+
exit 1
114+
fi
115+
if ! grep -q '"nix_conf_written":true' "$sandbox/bootstrap-1.json"; then
116+
printf 'vm-09: step 1 expected nix_conf_written:true:\n' >&2
117+
cat "$sandbox/bootstrap-1.json" >&2
118+
exit 1
119+
fi
120+
if ! grep -q 'experimental-features = nix-command flakes' "$nix_conf"; then
121+
printf 'vm-09: nix.conf does not contain the expected line:\n' >&2
122+
cat "$nix_conf" >&2
123+
exit 1
124+
fi
125+
126+
# Step 2: idempotent re-run.
127+
"$PEARLITE_BIN" \
128+
--format=json \
129+
--config-dir="$sandbox/repo" \
130+
--state-file="$sandbox/state.toml" \
131+
bootstrap \
132+
--host-file="$sandbox/repo/hosts/vm-test.ncl" \
133+
--installer-script="$script" \
134+
--nix-conf="$nix_conf" \
135+
> "$sandbox/bootstrap-2.json" || {
136+
printf 'vm-09: step 2 bootstrap failed:\n' >&2
137+
cat "$sandbox/bootstrap-2.json" >&2
138+
exit 1
139+
}
140+
141+
if ! grep -q '"install":"already"' "$sandbox/bootstrap-2.json"; then
142+
printf 'vm-09: step 2 install did not short-circuit:\n' >&2
143+
cat "$sandbox/bootstrap-2.json" >&2
144+
exit 1
145+
fi
146+
if ! grep -q '"nix_conf_written":false' "$sandbox/bootstrap-2.json"; then
147+
printf 'vm-09: step 2 expected nix_conf_written:false (idempotent):\n' >&2
148+
cat "$sandbox/bootstrap-2.json" >&2
149+
exit 1
150+
fi
151+
152+
printf 'vm-09: nix bootstrap idempotency PASS\n'

0 commit comments

Comments
 (0)