#!/usr/bin/env -S nu --no-config-file # Tests for bootstrap.nu using disposable HOME/repo fixtures and fake commands. # No hardware, network, or real brew/chezmoi/jj required. # # Run from the repository root: # nu --no-config-file tests/run_bootstrap_tests.nu use ../home/dot_config/nushell/scripts/dotfiles.nu brewfile-third-party-entries let REPO_ROOT = ($env.FILE_PWD | path dirname) const BASE_BREWFILE = 'brew "bat" brew "foo/tap/thing" cask "bar/tap/baz" cask "claude-code@latest" ' # Fake body for logging commands: records its own name plus arguments. const LOG_FAKE = '($env.CURRENT_FILE | path basename) + " " + ($rest | str join " ") + "\n" | save --append $env.FAKE_LOG' mut results: list = [] mut fixtures: list = [] def check [name: string, ok: bool, detail: string = ""] { {name: $name, ok: $ok, detail: $detail} } # Create an executable fake command in the fixture bin directory. The body is # Nushell and runs inside `def main [...rest: string]`. def make-fake-command [bin_dir: path, name: string, body: string] { let path = ($bin_dir | path join $name) $"#!/usr/bin/env -S nu --no-config-file\ndef main --wrapped [...rest: string] {\n ($body)\n}\n" | save --force $path chmod 755 $path } # Build a disposable HOME with a minimal but valid repository layout at # ~/.local/share/chezmoi, plus a bin directory of fake commands. def make-fixture [brewfile: string] { let root = (mktemp --directory --tmpdir "bootstrap-test.XXXXXX") let home_dir = ($root | path join "home") let repo = ($home_dir | path join ".local" "share" "chezmoi") mkdir $home_dir mkdir ($repo | path join ".jj") mkdir ($repo | path join "home" "dot_config" "nushell" "scripts") mkdir ($repo | path join "secrets") "home\n" | save --force ($repo | path join ".chezmoiroot") "# test fixture; the real template prompts and is not exercised here\n" | save --force ($repo | path join "home" ".chezmoi.toml.tmpl") $brewfile | save --force ($repo | path join "home" "dot_Brewfile") "# fixture placeholder; only existence is validated\n" | save --force ($repo | path join "secrets" "ssh.sops.yaml") for module in [dotfiles.nu dotfiles-secrets.nu] { cp ($REPO_ROOT | path join "home" "dot_config" "nushell" "scripts" $module) ($repo | path join "home" "dot_config" "nushell" "scripts" $module) } cp ($REPO_ROOT | path join "bootstrap.nu") ($repo | path join "bootstrap.nu") let bin_dir = ($root | path join "bin") mkdir $bin_dir for name in [brew chezmoi jj fc-cache rustup] { make-fake-command $bin_dir $name $LOG_FAKE } make-fake-command $bin_dir fc-match 'if (($env.FC_MATCH_FORCE? | default "") | is-not-empty) { print $env.FC_MATCH_FORCE } else { print ($rest | last) }' {root: $root, home: $home_dir, repo: $repo, bin: $bin_dir, log: ($root | path join "fake.log")} } # Run the fixture's bootstrap.nu copy with a disposable HOME and a PATH limited # to the fake bin directory plus core tools. Returns the `complete` record. def run-bootstrap [fixture: record, arguments: list, extra_env: record = {}] { let nu_dir = ((which nu | first).path | path dirname) let path = [$fixture.bin $nu_dir "/usr/bin" "/bin"] let environment = ({HOME: $fixture.home, PATH: $path, FAKE_LOG: $fixture.log} | merge $extra_env) with-env $environment { do { ^nu --no-config-file ($fixture.repo | path join "bootstrap.nu") ...$arguments } | complete } } def read-log [fixture: record] { if ($fixture.log | path exists) { open --raw $fixture.log } else { "" } } def font-drive [fixture: record, files: list] { let drive = ($fixture.root | path join "font-drive") for file in $files { let target = ($drive | path join $file.path) mkdir ($target | path dirname) $file.content | save --force $target } $drive } # --- T1: --plan lists the stages without touching anything ------------------- let fixture = (make-fixture $BASE_BREWFILE) $fixtures = ($fixtures | append $fixture.root) let result = (run-bootstrap $fixture [--plan]) $results = ($results | append (check "plan lists stages and exits zero" ($result.exit_code == 0 and ($result.stdout | str contains "Validate the cloned jj/chezmoi repository layout")) ($result.stderr | str trim))) # --- T2: trust entries derive from the Brewfile ------------------------------ let entries = (with-env {HOME: $fixture.home} { brewfile-third-party-entries }) $results = ($results | append (check "third-party trust entries derive from the Brewfile" ($entries == [[kind name]; [brew "foo/tap/thing"] [cask "bar/tap/baz"]]) ($entries | to json))) # --- T3: package stage trusts derived entries, never official ones ----------- let result = (run-bootstrap $fixture [--yes --skip-fonts --skip-secrets --skip-themes --test-stop-after packages]) let log = (read-log $fixture) $results = ($results | append (check "package stage succeeds with fakes" ($result.exit_code == 0) ($result.stderr | str trim))) $results = ($results | append (check "trusts derived third-party formula" ($log | str contains "brew trust --formula foo/tap/thing") $log)) $results = ($results | append (check "trusts derived third-party cask" ($log | str contains "brew trust --cask bar/tap/baz") $log)) $results = ($results | append (check "never trusts official brews" (not ($log | str contains "trust --formula bat")) $log)) $results = ($results | append (check "runs brew bundle against the source Brewfile" ($log | str contains "brew bundle --no-upgrade") $log)) # --- T4: cargo stage initializes a toolchain when cargo is not ready ---------- let fixture = (make-fixture ($BASE_BREWFILE + 'cargo "somepkg"\n')) $fixtures = ($fixtures | append $fixture.root) make-fake-command $fixture.bin cargo '($env.CURRENT_FILE | path basename) + " " + ($rest | str join " ") + "\n" | save --append $env.FAKE_LOG let ready = ($env.FAKE_CARGO_READY | path exists) if $rest == [--version] and not $ready { exit 1 } if $rest == [--version] { print "cargo 1.85.0" } if $rest == [install --list] and $ready { print "" }' make-fake-command $fixture.bin rustup '($env.CURRENT_FILE | path basename) + " " + ($rest | str join " ") + "\n" | save --append $env.FAKE_LOG if $rest == [default stable] { touch $env.FAKE_CARGO_READY }' let result = (run-bootstrap $fixture [--yes --skip-fonts --skip-secrets --skip-themes --test-stop-after packages] {FAKE_CARGO_READY: ($fixture.root | path join "cargo-ready")}) let log = (read-log $fixture) let rustup_at = ($log | str index-of "rustup default stable") let install_at = ($log | str index-of "cargo install --locked somepkg") $results = ($results | append (check "cargo stage initializes rustup before installing a crate" ($result.exit_code == 0 and $rustup_at >= 0 and $install_at > $rustup_at) ($result.stderr | str trim))) # --- T5: cargo stage stays out of the way when cargo works ------------------- let fixture = (make-fixture ($BASE_BREWFILE + 'cargo "somepkg"\n')) $fixtures = ($fixtures | append $fixture.root) make-fake-command $fixture.bin cargo '($env.CURRENT_FILE | path basename) + " " + ($rest | str join " ") + "\n" | save --append $env.FAKE_LOG if $rest == [--version] { print "cargo 1.85.0" } if $rest == [install --list] { print "somepkg v1.0.0:" }' let result = (run-bootstrap $fixture [--yes --skip-fonts --skip-secrets --skip-themes --test-stop-after packages]) let log = (read-log $fixture) $results = ($results | append (check "cargo stage skips rustup and an installed crate" ($result.exit_code == 0 and not ($log | str contains "rustup default stable") and not ($log | str contains "cargo install --locked")) ($result.stderr | str trim))) # --- T6: font restore accepts uppercase extensions --------------------------- let fixture = (make-fixture $BASE_BREWFILE) $fixtures = ($fixtures | append $fixture.root) let drive = (font-drive $fixture [{path: "PragmataProMono.OTF", content: "fake otf"} {path: "BerkeleyMono.TTF", content: "fake ttf"}]) let installed = ($fixture.home | path join ".local" "share" "fonts") let result = (run-bootstrap $fixture [--yes --font-source $drive --skip-secrets --skip-themes --test-stop-after fonts]) $results = ($results | append (check "uppercase font extensions are installed" ($result.exit_code == 0 and ($installed | path join "PragmataProMono.OTF" | path exists) and ($installed | path join "BerkeleyMono.TTF" | path exists)) ($result.stderr | str trim))) # --- T7: font fallback resolution is rejected -------------------------------- let fixture = (make-fixture $BASE_BREWFILE) $fixtures = ($fixtures | append $fixture.root) let drive = (font-drive $fixture [{path: "PragmataProMono.otf", content: "fake otf"}]) let result = (run-bootstrap $fixture [--yes --font-source $drive --skip-secrets --skip-themes --test-stop-after fonts] {FC_MATCH_FORCE: "DejaVu Sans Mono"}) $results = ($results | append (check "fallback font resolution fails the stage" ($result.exit_code != 0 and (($result.stdout + $result.stderr) | str contains "fallback family")))) # --- T8: duplicate font basenames are rejected ------------------------------- let fixture = (make-fixture $BASE_BREWFILE) $fixtures = ($fixtures | append $fixture.root) let drive = (font-drive $fixture [{path: "a/dup.otf", content: "one"} {path: "b/dup.otf", content: "two"}]) let result = (run-bootstrap $fixture [--yes --font-source $drive --skip-secrets --skip-themes --test-stop-after fonts]) $results = ($results | append (check "duplicate font basenames fail the stage" ($result.exit_code != 0 and (($result.stdout + $result.stderr) | str contains "duplicate filenames")))) # --- T9: PC/SC environment set by preflight persists in main ---------------- # validate-pcsc is an `--env` function because ordinary Nushell function # environment changes disappear on return. Probe the same contract directly so # a regression cannot silently send Homebrew-linked clients to the wrong socket. let pcsc_probe = ( env -u PCSCLITE_CSOCK_NAME nu --no-config-file -c ' const socket = "/run/pcscd/pcscd.comm" def --env set-pcsc [] { $env.PCSCLITE_CSOCK_NAME = $socket } set-pcsc $env.PCSCLITE_CSOCK_NAME ' | str trim ) $results = ($results | append (check "PCSC environment persists after preflight" ($pcsc_probe == "/run/pcscd/pcscd.comm") $pcsc_probe)) # --- cleanup and summary ----------------------------------------------------- for root in $fixtures { rm --recursive --force $root } print "" for result in $results { let status = if $result.ok { "PASS" } else { "FAIL" } let suffix = if ($result.detail | is-not-empty) and not $result.ok { $" — ($result.detail)" } else { "" } print $"($status) ($result.name)($suffix)" } let failures = ($results | where {|result| not $result.ok }) print "" if ($failures | is-empty) { print "All bootstrap tests passed." } else { print $"($failures | length) tests failed." exit 1 }