From b951a758d52a409576e1a5b529f837a376933af5 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:37:56 +0800 Subject: [PATCH 01/18] test: add test-e2e-fixture preset for VM e2e tests --- internal/config/data/presets.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/config/data/presets.yaml b/internal/config/data/presets.yaml index e13762a..0f867c5 100644 --- a/internal/config/data/presets.yaml +++ b/internal/config/data/presets.yaml @@ -29,6 +29,15 @@ presets: - stats - rectangle + test-e2e-fixture: + name: test-e2e-fixture + description: "Minimal fixture for VM e2e tests — not for real use" + cli: + - jq + - bat + - ripgrep + cask: [] + developer: name: developer description: Ready-to-code Mac setup (Node, Go, Docker, VS Code) From 26e7c380b369673a0df60fef0fcee19c479318de Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:41:14 +0800 Subject: [PATCH 02/18] test(vm): use test-e2e-fixture preset in FullSetupConfiguresEverything and Edge_ShellActuallyWorks --- test/e2e/vm_edge_cases_test.go | 2 +- test/e2e/vm_user_journey_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/vm_edge_cases_test.go b/test/e2e/vm_edge_cases_test.go index 594231b..ac34fee 100644 --- a/test/e2e/vm_edge_cases_test.go +++ b/test/e2e/vm_edge_cases_test.go @@ -33,7 +33,7 @@ func TestVM_Edge_ShellActuallyWorks(t *testing.T) { bin := vmCopyDevBinary(t, vm) // Install with shell setup - _, err := vmRunDevBinaryWithGit(t, vm, bin, "install --preset minimal --silent --shell install --dotfiles skip --macos skip") + _, err := vmRunDevBinaryWithGit(t, vm, bin, "install --preset test-e2e-fixture --silent --shell install --dotfiles skip --macos skip") require.NoError(t, err) t.Run("zsh_login_shell_starts", func(t *testing.T) { diff --git a/test/e2e/vm_user_journey_test.go b/test/e2e/vm_user_journey_test.go index d66546a..fe3e350 100644 --- a/test/e2e/vm_user_journey_test.go +++ b/test/e2e/vm_user_journey_test.go @@ -172,7 +172,7 @@ func TestVM_Journey_FullSetupConfiguresEverything(t *testing.T) { bin := vmCopyDevBinary(t, vm) output, err := vmRunDevBinaryWithGit(t, vm, bin, - "install --preset minimal --silent --shell install --dotfiles clone --macos configure") + "install --preset test-e2e-fixture --silent --shell install --dotfiles clone --macos configure") t.Logf("full setup:\n%s", output) require.NoError(t, err) From 704f671c1fb0a65b111d39e7057564a5491d6937 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:43:35 +0800 Subject: [PATCH 03/18] test(vm): move FullSetupConfiguresEverything to VM_B parallel slot --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2639b71..8b5bccd 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,9 @@ install-hooks uninstall-hooks # VM A: install/journey tests that touch real system state (longest-running). -VM_A_TESTS := TestVM_Journey_FirstTimeUser|TestVM_Journey_DryRunIsCompletelySafe|TestVM_Journey_FullSetupConfiguresEverything|TestVM_Interactive_InstallScript +VM_A_TESTS := TestVM_Journey_FirstTimeUser|TestVM_Journey_DryRunIsCompletelySafe|TestVM_Interactive_InstallScript # VM B: all other VM tests — dotfiles, macOS, edge cases, smoke, real-install, sync. -VM_B_TESTS := TestVM_Journey_Dotfiles|TestVM_Journey_MacOS|TestVM_Edge_|TestSmoke_|TestE2E_ +VM_B_TESTS := TestVM_Journey_Dotfiles|TestVM_Journey_MacOS|TestVM_Journey_FullSetupConfiguresEverything|TestVM_Edge_|TestSmoke_|TestE2E_ BINARY_NAME=openboot BINARY_PATH=./$(BINARY_NAME) From 1aa5feb11db82b3cab55377724e6a2c29e14a02e Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:46:04 +0800 Subject: [PATCH 04/18] docs: update L4 canonical command to test-vm-parallel (~14 min) --- docs/HARNESS.md | 2 +- scripts/vm/README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/HARNESS.md b/docs/HARNESS.md index 4151040..b300222 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -48,7 +48,7 @@ Three regulation categories: | Behav. | L1 unit + integration + contract (faked runners *and* real brew/git/npm in temp dirs) | pre-push, CI | `make test-unit` | | Behav. | L2 contract schema (against openboot-contract repo) | CI | `.github/workflows/test.yml` `contract` job | | Behav. | L3 e2e binary | release | `make test-e2e` | -| Behav. | L4 VM e2e (`vm`) — runs full destructive suite in a local Tart VM | local only (convention is pre-release; no CI gate) | `make test-vm` (driver: `scripts/vm/run.sh`) | +| Behav. | L4 VM e2e (`vm`) — runs full destructive suite in a local Tart VM | local only (convention is pre-release; no CI gate) | `make test-vm-parallel` (~14 min, driver: `scripts/vm/run.sh`); `make test-vm` is the serial single-VM fallback | | Behav. | curl\|bash smoke (install.sh + mock server) | every PR | `.github/workflows/test.yml` `curl-bash-smoke` job | | Behav. | Auto-release sensor — patch fast lane (`fix:`-only) auto-tags + dispatches `release.yml`; feat threshold opens a `release-ready` issue with a `make test-vm` checklist instead | push to `main` | `.github/workflows/auto-release.yml` | | Behav. | Release notes — Conventional Commits since previous tag, grouped by type (Features / Bug Fixes / etc) + Full Changelog link, appended to the install-instructions template | tag push or `workflow_dispatch` | `.github/workflows/release.yml` (`Write release notes` step) | diff --git a/scripts/vm/README.md b/scripts/vm/README.md index 74ae73b..d4216d8 100644 --- a/scripts/vm/README.md +++ b/scripts/vm/README.md @@ -27,7 +27,8 @@ Verify with `tart list` — you should see `macos-tahoe-base`. ## Running ```bash -make test-vm # full suite (~30 min) +make test-vm-parallel # full suite (~14 min, 2 parallel VMs) — use before tagging +make test-vm # serial fallback — single VM, useful for debugging make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test ``` From 2ca965d42d0f601af76764e1330897fe88f97afe Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:48:11 +0800 Subject: [PATCH 05/18] docs: update stale make test-vm references to test-vm-parallel --- .github/workflows/auto-release.yml | 2 +- CLAUDE.md | 3 ++- CONTRIBUTING.md | 9 +++++---- docs/HARNESS.md | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 62e812e..07bce17 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -176,7 +176,7 @@ jobs: **not** auto-tag. Run the destructive e2e suite locally, then cut the release manually: - - [ ] \`make test-vm\` passes (Apple Silicon + Tart required — see scripts/vm/README.md) + - [ ] \`make test-vm-parallel\` passes (Apple Silicon + Tart required — see scripts/vm/README.md) - [ ] sanity-check the curl|bash smoke and cli-compat results in the most recent test.yml run on main - [ ] \`git tag -a ${NEW_TAG} -m "..."\` and \`git push origin ${NEW_TAG}\` - [ ] close this issue diff --git a/CLAUDE.md b/CLAUDE.md index 0f75da2..95d5d29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,8 @@ make build-release VERSION=0.25.0 # optimized + UPX # Test — full tier table in CONTRIBUTING.md make test-unit # L1 (~75s) — unit + integration + contract; pre-push hook make test-e2e # L3 compiled binary -make test-vm # L4 (~30m) — destructive e2e in a local Tart VM; before tagging +make test-vm-parallel # L4 (~14 min) — destructive e2e in 2 parallel Tart VMs; before tagging +make test-vm # L4 serial fallback — single VM, use for debugging make test-coverage # coverage.out + coverage.html # Single test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66e207b..ffb28ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,13 +37,13 @@ Tests are split across four tiers. Which one runs where: | **L1 Unit + Integration + Contract** | Pure-Go logic with faked `Runner` *plus* real `brew` / `git` / `npm` against temp dirs and real `httptest` servers | `make test-unit` (~75s) | Every push (pre-push hook); CI on push/PR | | **L2 Contract schema** | JSON schema validation against [openboot-contract](https://github.com/openbootdotdev/openboot-contract) | (runs in CI only) | CI on push/PR | | **L3 E2E binary** | Compiled binary driven by scripts; `-tags=e2e` | `make test-e2e` | CI on release | -| **L4 VM e2e** | Full destructive suite (`-tags="e2e,vm"`) runs inside an ephemeral Tart VM provisioned by `scripts/vm/run.sh`. Installs real packages, modifies `~/.zshrc`, writes `defaults` — all contained to the throwaway VM. | `make test-vm` (~30 min, Apple Silicon + Tart required) | **Local only** — convention is to run before tagging a release. No CI gate. | +| **L4 VM e2e** | Full destructive suite (`-tags="e2e,vm"`) runs inside an ephemeral Tart VM provisioned by `scripts/vm/run.sh`. Installs real packages, modifies `~/.zshrc`, writes `defaults` — all contained to the throwaway VM. | `make test-vm-parallel` (~14 min, Apple Silicon + Tart required); `make test-vm` is the serial single-VM fallback | **Local only** — convention is to run before tagging a release. No CI gate. | Rules of thumb: - **Local dev:** run nothing manually if hooks are installed. `make test-unit` on demand when you want a sanity check. Skip L2+ unless you're cutting a release. - **Before pushing:** `make test-unit` (the pre-push hook does this automatically). Requires `brew` / `git` / `npm` on PATH — they are queried read-only against temp dirs, no real installs. -- **Before tagging a release (convention, not enforced):** `make test-vm` on an Apple Silicon Mac with Tart installed. See [VM E2E setup](#vm-e2e-setup) below. `auto-release.yml` opens a `release-ready` issue on `feat:` thresholds to nudge you here. +- **Before tagging a release (convention, not enforced):** `make test-vm-parallel` on an Apple Silicon Mac with Tart installed. See [VM E2E setup](#vm-e2e-setup) below. `auto-release.yml` opens a `release-ready` issue on `feat:` thresholds to nudge you here. ## VM E2E setup @@ -59,9 +59,10 @@ tart clone ghcr.nju.edu.cn/cirruslabs/macos-tahoe-base:latest macos-tahoe-base Then: ```bash -make test-vm # full suite (~30 min) +make test-vm-parallel # full suite in parallel (~14 min) +make test-vm # full suite serial fallback (~30 min) make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test -OPENBOOT_VM_KEEP=1 make test-vm # don't destroy VM at exit (debug) +OPENBOOT_VM_KEEP=1 make test-vm-parallel # don't destroy VMs at exit (debug) ``` See `scripts/vm/README.md` for full environment-variable docs and diff --git a/docs/HARNESS.md b/docs/HARNESS.md index b300222..3f452b4 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -113,7 +113,7 @@ it survives doc rot. - **No CI gate for VM e2e.** Apple Silicon Tart VMs don't run on GitHub-hosted `macos-latest` runners (no nested virt, wrong arch guarantees), and we declined to set up a self-hosted runner. L4 is - local-only. Running `make test-vm` before tagging is convention, + local-only. Running `make test-vm-parallel` before tagging is convention, encoded as a `release-ready` issue opened by `auto-release.yml` on `feat:` thresholds — not a hard gate. A human can release without it. From 407a8215076fa88015ac0e58c852a1e9087c45f9 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:49:06 +0800 Subject: [PATCH 06/18] docs: fix missed test-vm reference in HARNESS.md auto-release row --- docs/HARNESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/HARNESS.md b/docs/HARNESS.md index 3f452b4..a9ca46c 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -50,7 +50,7 @@ Three regulation categories: | Behav. | L3 e2e binary | release | `make test-e2e` | | Behav. | L4 VM e2e (`vm`) — runs full destructive suite in a local Tart VM | local only (convention is pre-release; no CI gate) | `make test-vm-parallel` (~14 min, driver: `scripts/vm/run.sh`); `make test-vm` is the serial single-VM fallback | | Behav. | curl\|bash smoke (install.sh + mock server) | every PR | `.github/workflows/test.yml` `curl-bash-smoke` job | -| Behav. | Auto-release sensor — patch fast lane (`fix:`-only) auto-tags + dispatches `release.yml`; feat threshold opens a `release-ready` issue with a `make test-vm` checklist instead | push to `main` | `.github/workflows/auto-release.yml` | +| Behav. | Auto-release sensor — patch fast lane (`fix:`-only) auto-tags + dispatches `release.yml`; feat threshold opens a `release-ready` issue with a `make test-vm-parallel` checklist instead | push to `main` | `.github/workflows/auto-release.yml` | | Behav. | Release notes — Conventional Commits since previous tag, grouped by type (Features / Bug Fixes / etc) + Full Changelog link, appended to the install-instructions template | tag push or `workflow_dispatch` | `.github/workflows/release.yml` (`Write release notes` step) | | Behav. | Old-CLI compat (previous release × current mock server) | every PR | `.github/workflows/test.yml` `cli-compat` job | | Feedfwd. | Agent conventions | every AI turn | `CLAUDE.md`, `AGENTS.md` | From b01c55e2d7652b4ae690e21fec3056c657239315 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Mon, 18 May 2026 23:50:27 +0800 Subject: [PATCH 07/18] docs: fix remaining stale test-vm references in auto-release.yml and README --- .github/workflows/auto-release.yml | 2 +- scripts/vm/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 07bce17..309ae5e 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -181,7 +181,7 @@ jobs: - [ ] \`git tag -a ${NEW_TAG} -m "..."\` and \`git push origin ${NEW_TAG}\` - [ ] close this issue - Skipping \`make test-vm\` is allowed (it is not a hard gate), + Skipping \`make test-vm-parallel\` is allowed (it is not a hard gate), but \`feat:\` changes carry more risk than \`fix:\` patches. EOF ) diff --git a/scripts/vm/README.md b/scripts/vm/README.md index d4216d8..22c4029 100644 --- a/scripts/vm/README.md +++ b/scripts/vm/README.md @@ -1,7 +1,7 @@ # Tart VM e2e The `scripts/vm/run.sh` driver spins up an ephemeral Tart VM for the -destructive e2e suite (`make test-vm`). The 12 test files in `test/e2e/` +destructive e2e suite (`make test-vm-parallel`). The 12 test files in `test/e2e/` run inside it. ## One-time setup From 5ee3c9ba20ccd9f15e4c58e10b081b1bf0755f31 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 00:04:47 +0800 Subject: [PATCH 08/18] chore(vm): rename base image from macos-tahoe-base to tahoe-base --- CONTRIBUTING.md | 4 ++-- scripts/vm/README.md | 18 +++++++++--------- scripts/vm/run.sh | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ffb28ef..d9f0b12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,8 +52,8 @@ on an Apple Silicon Mac: ```bash brew install cirruslabs/cli/tart -tart pull ghcr.nju.edu.cn/cirruslabs/macos-tahoe-base:latest -tart clone ghcr.nju.edu.cn/cirruslabs/macos-tahoe-base:latest macos-tahoe-base +tart pull ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest +tart clone ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest tahoe-base ``` Then: diff --git a/scripts/vm/README.md b/scripts/vm/README.md index 22c4029..e1c7f46 100644 --- a/scripts/vm/README.md +++ b/scripts/vm/README.md @@ -14,15 +14,15 @@ brew install cirruslabs/cli/tart # 2. Pull the base image (downloads ~25GB) # Canonical upstream: -tart pull ghcr.io/cirruslabs/macos-tahoe-base:latest +tart pull ghcr.io/cirruslabs/tahoe-base:latest # Faster mirror for users in China: -# tart pull ghcr.nju.edu.cn/cirruslabs/macos-tahoe-base:latest +# tart pull ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest # 3. Give it the local name run.sh expects -tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest macos-tahoe-base +tart clone ghcr.io/cirruslabs/tahoe-base:latest tahoe-base ``` -Verify with `tart list` — you should see `macos-tahoe-base`. +Verify with `tart list` — you should see `tahoe-base`. ## Running @@ -36,12 +36,12 @@ make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test | Var | Default | Effect | |---|---|---| -| `OPENBOOT_VM_BASE` | `macos-tahoe-base` | Local Tart image to clone from | +| `OPENBOOT_VM_BASE` | `tahoe-base` | Local Tart image to clone from | | `OPENBOOT_VM_KEEP` | `0` | When `1`, do not destroy the VM at exit. Useful for debugging — attach with the SSH key path printed at exit. Remember to clean up manually. | ## Troubleshooting -- **`base image 'macos-tahoe-base' not found locally`** — run the one-time +- **`base image 'tahoe-base' not found locally`** — run the one-time setup above. - **SSH not reachable within 60s** — Tart logs dumped to stderr. Try `OPENBOOT_VM_KEEP=1 make test-vm` (it'll still fail at boot, but the @@ -59,9 +59,9 @@ make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test - **Base image needs updating** — re-pull and re-clone: ```bash - tart delete macos-tahoe-base - tart pull ghcr.io/cirruslabs/macos-tahoe-base:latest - tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest macos-tahoe-base + tart delete tahoe-base + tart pull ghcr.io/cirruslabs/tahoe-base:latest + tart clone ghcr.io/cirruslabs/tahoe-base:latest tahoe-base ``` ## Why a base image (not vanilla) diff --git a/scripts/vm/run.sh b/scripts/vm/run.sh index 7038d29..614181a 100755 --- a/scripts/vm/run.sh +++ b/scripts/vm/run.sh @@ -5,7 +5,7 @@ # scripts/vm/run.sh # # Env vars: -# OPENBOOT_VM_BASE — local Tart image name to clone (default: macos-tahoe-base) +# OPENBOOT_VM_BASE — local Tart image name to clone (default: tahoe-base) # OPENBOOT_VM_KEEP — if "1", do not destroy the VM at exit (for debugging) # # Exit codes match the in-VM make target. @@ -19,7 +19,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [ $# -ge 1 ] || die "usage: $0 " TARGET="$1" -BASE="${OPENBOOT_VM_BASE:-macos-tahoe-base}" +BASE="${OPENBOOT_VM_BASE:-tahoe-base}" KEEP="${OPENBOOT_VM_KEEP:-0}" VM_TEST="${OPENBOOT_VM_TEST:-}" VM="openboot-ephemeral-$$" @@ -71,7 +71,7 @@ tart exec "$VM" sh -c "printf '%s\n' '$PUBKEY' >> /Users/admin/.ssh/authorized_k tart exec "$VM" chmod 700 /Users/admin/.ssh tart exec "$VM" chmod 600 /Users/admin/.ssh/authorized_keys -# Ensure Go is available via mise (the macos-tahoe-base image ships mise but +# Ensure Go is available via mise (the tahoe-base image ships mise but # not Go pre-activated in the default shell PATH). Both commands are idempotent. tart exec "$VM" sh -c '/opt/homebrew/bin/mise install go@latest && /opt/homebrew/bin/mise use -g go@latest' From 4ad1f49185c23e6eff5f5c58d4979770ef5935a1 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 08:53:51 +0800 Subject: [PATCH 09/18] test(vm): remove redundant VM e2e tests Delete smoke_test.go and real_install_test.go (covered by FirstTimeUser and DryRunIsCompletelySafe). Remove MacOSDefaults_ScreenshotsDirCreated and MacOSDefaults_DryRunWritesNothing (covered by FullSetupConfiguresEverything and DryRunIsCompletelySafe). Switch AllCategoriesWritten to test-e2e-fixture since macOS prefs don't depend on which packages are installed. --- test/e2e/macos_defaults_e2e_test.go | 65 +------------- test/e2e/real_install_test.go | 128 ---------------------------- test/e2e/smoke_test.go | 103 ---------------------- 3 files changed, 2 insertions(+), 294 deletions(-) delete mode 100644 test/e2e/real_install_test.go delete mode 100644 test/e2e/smoke_test.go diff --git a/test/e2e/macos_defaults_e2e_test.go b/test/e2e/macos_defaults_e2e_test.go index d4ddfb4..bfdddbf 100644 --- a/test/e2e/macos_defaults_e2e_test.go +++ b/test/e2e/macos_defaults_e2e_test.go @@ -22,6 +22,7 @@ import ( "github.com/openbootdotdev/openboot/testutil" ) + // macOSPrefCheck describes a single `defaults read` assertion. type macOSPrefCheck struct { domain string @@ -49,7 +50,7 @@ func TestVM_Journey_MacOSDefaults_AllCategoriesWritten(t *testing.T) { bin := vmCopyDevBinary(t, vm) output, err := vmRunDevBinaryWithGit(t, vm, bin, - "install --preset minimal --silent --shell skip --dotfiles skip --macos configure") + "install --preset test-e2e-fixture --silent --shell skip --dotfiles skip --macos configure") t.Logf("macOS configure output:\n%s", output) require.NoError(t, err, "install with --macos configure should succeed") @@ -137,65 +138,3 @@ func TestVM_Journey_MacOSDefaults_AllCategoriesWritten(t *testing.T) { } } -// TestVM_Journey_MacOSDefaults_ScreenshotsDirCreated verifies that the -// ~/Screenshots directory is created during a macOS configure run. -// -// Gap: the Screenshots directory creation (macos.CreateScreenshotsDir) was -// only checked in TestVM_Journey_FullSetupConfiguresEverything as part of a -// larger setup run. This test isolates that behaviour. -func TestVM_Journey_MacOSDefaults_ScreenshotsDirCreated(t *testing.T) { - if testing.Short() { - t.Skip("skipping macOS screenshots dir test in short mode") - } - - vm := testutil.NewMacHost(t) - vmInstallHomebrew(t, vm) - bin := vmCopyDevBinary(t, vm) - - // Remove ~/Screenshots if it already exists so the test is authoritative. - _, _ = vm.Run("rm -rf ~/Screenshots") - - _, err := vmRunDevBinaryWithGit(t, vm, bin, - "install --preset minimal --silent --shell skip --dotfiles skip --macos configure") - require.NoError(t, err, "install with --macos configure should succeed") - - out, _ := vm.Run("test -d ~/Screenshots && echo exists || echo missing") - assert.Contains(t, out, "exists", - "~/Screenshots should be created by --macos configure") -} - -// TestVM_Journey_MacOSDefaults_DryRunWritesNothing verifies that -// --dry-run --macos configure does NOT modify any macOS preference. -// -// Regression guard: a bug in the Configure() dry-run branch could silently -// write preferences on dry-run. -func TestVM_Journey_MacOSDefaults_DryRunWritesNothing(t *testing.T) { - if testing.Short() { - t.Skip("skipping macOS dry-run defaults test in short mode") - } - - vm := testutil.NewMacHost(t) - vmInstallHomebrew(t, vm) - bin := vmCopyDevBinary(t, vm) - - // Force a known value that differs from what --macos configure would write - // ("Always"). This makes the assertion non-vacuous: if dry-run accidentally - // applies the preference, the value changes to "Always" and the test fails. - _, err := vm.Run(`defaults write "NSGlobalDomain" "AppleShowScrollBars" -string "WhenScrolling"`) - require.NoError(t, err, "should be able to force a known test value before dry-run") - - before, _ := vm.Run( - `defaults read "NSGlobalDomain" "AppleShowScrollBars" 2>/dev/null || echo UNSET`, - ) - - _, err = vmRunDevBinaryWithGit(t, vm, bin, - "install --preset minimal --silent --shell skip --dotfiles skip --macos configure --dry-run") - require.NoError(t, err, "dry-run should succeed") - - after, _ := vm.Run( - `defaults read "NSGlobalDomain" "AppleShowScrollBars" 2>/dev/null || echo UNSET`, - ) - - assert.Equal(t, strings.TrimSpace(before), strings.TrimSpace(after), - "dry-run must not change NSGlobalDomain/AppleShowScrollBars") -} diff --git a/test/e2e/real_install_test.go b/test/e2e/real_install_test.go deleted file mode 100644 index 344d771..0000000 --- a/test/e2e/real_install_test.go +++ /dev/null @@ -1,128 +0,0 @@ -//go:build e2e && vm - -package e2e - -import ( - "encoding/json" - "os" - "os/exec" - "strings" - "testing" - - "github.com/openbootdotdev/openboot/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestE2E_InstallSinglePackage_JQ(t *testing.T) { - // Given: system does not have jq installed - testutil.UninstallPackage(t, "jq") - if testutil.IsPackageInstalled("jq") { - t.Skip("jq cannot be uninstalled (likely a dependency); skipping") - } - binary := testutil.BuildTestBinary(t) - - // When: we install jq via openboot (minimal preset includes jq) - cmd := exec.Command(binary, "install", "--packages-only", "--silent", "--preset", "minimal") - cmd.Env = append(os.Environ(), - "OPENBOOT_GIT_NAME=Test User", - "OPENBOOT_GIT_EMAIL=test@example.com", - ) - - output, err := cmd.CombinedOutput() - t.Logf("Install output: %s", string(output)) - - // Then: jq should be installed and executable - assert.NoError(t, err, "installation should succeed") - assert.True(t, testutil.IsPackageInstalled("jq"), "jq should be installed") - - jqPath, err := exec.Command("which", "jq").Output() - require.NoError(t, err, "jq should be in PATH") - assert.Contains(t, string(jqPath), "jq") - - jqVersion, err := exec.Command("jq", "--version").Output() - require.NoError(t, err, "jq should be executable") - assert.Contains(t, string(jqVersion), "jq-") - - // Cleanup - testutil.UninstallPackage(t, "jq") -} - -func TestE2E_InstallMultiplePackages(t *testing.T) { - packages := []string{"bat", "fd"} - - // Given: packages are not installed - for _, pkg := range packages { - testutil.EnsurePackageNotInstalled(t, pkg) - } - binary := testutil.BuildTestBinary(t) - - // When: we install multiple packages via the minimal preset (includes bat + fd) - cmd := exec.Command(binary, "install", "--packages-only", "--silent", "--preset", "minimal") - cmd.Env = append(os.Environ(), - "PATH=/opt/homebrew/bin:/opt/homebrew/sbin:"+os.Getenv("PATH"), - "OPENBOOT_GIT_NAME=Test User", - "OPENBOOT_GIT_EMAIL=test@example.com", - ) - - output, err := cmd.CombinedOutput() - t.Logf("Install output: %s", string(output)) - - // Then: all packages should be installed - assert.NoError(t, err, "installation should succeed") - - for _, pkg := range packages { - assert.True(t, testutil.IsPackageInstalled(pkg), "%s should be installed", pkg) - - pkgPath, err := exec.Command("which", pkg).Output() - require.NoError(t, err, "%s should be in PATH", pkg) - assert.Contains(t, string(pkgPath), pkg) - } - - // Cleanup - for _, pkg := range packages { - testutil.UninstallPackage(t, pkg) - } -} - -func TestE2E_SnapshotCapture_RecordsInstalledPackage(t *testing.T) { - binary := testutil.BuildTestBinary(t) - testPkg := "ripgrep" - - // Given: ripgrep is installed - if !testutil.IsPackageInstalled(testPkg) { - cmd := exec.Command("/opt/homebrew/bin/brew", "install", testPkg) - require.NoError(t, cmd.Run(), "test setup: should install ripgrep") - } - - // When: we capture a snapshot - cmd := exec.Command(binary, "snapshot", "--json") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - require.NoError(t, err, "snapshot capture should succeed, stderr: %s", stderr.String()) - - snapshotJSON := stdout.String() - var snapshot map[string]interface{} - err = json.Unmarshal([]byte(snapshotJSON), &snapshot) - require.NoError(t, err, "snapshot should be valid JSON") - - // Then: snapshot should contain ripgrep in formulae - packages, ok := snapshot["packages"].(map[string]interface{}) - require.True(t, ok, "snapshot should have packages field") - - brew, ok := packages["formulae"].([]interface{}) - require.True(t, ok, "snapshot should have formulae packages") - - foundRipgrep := false - for _, pkg := range brew { - if pkgStr, ok := pkg.(string); ok && strings.Contains(pkgStr, "ripgrep") { - foundRipgrep = true - break - } - } - assert.True(t, foundRipgrep, "snapshot should contain ripgrep") -} - diff --git a/test/e2e/smoke_test.go b/test/e2e/smoke_test.go deleted file mode 100644 index 144ed3c..0000000 --- a/test/e2e/smoke_test.go +++ /dev/null @@ -1,103 +0,0 @@ -//go:build e2e && vm - -package e2e - -import ( - "encoding/json" - "os" - "os/exec" - "strings" - "testing" - - "github.com/openbootdotdev/openboot/internal/snapshot" - "github.com/openbootdotdev/openboot/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func captureSnapshot(t *testing.T, binary string) snapshot.Snapshot { - t.Helper() - - cmd := exec.Command(binary, "snapshot", "--json") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - require.NoError(t, err, "snapshot capture failed, stderr: %s", stderr.String()) - - var snap snapshot.Snapshot - err = json.Unmarshal([]byte(stdout.String()), &snap) - require.NoError(t, err, "snapshot JSON parse failed") - - return snap -} - -func TestSmoke_InstallAndVerifySnapshot(t *testing.T) { - binary := testutil.BuildTestBinary(t) - testPkg := "cowsay" - - // Given: cowsay is not installed - testutil.EnsurePackageNotInstalled(t, testPkg) - - // Capture before snapshot - before := captureSnapshot(t, binary) - beforeFormulae := make(map[string]bool, len(before.Packages.Formulae)) - for _, f := range before.Packages.Formulae { - beforeFormulae[f] = true - } - require.False(t, beforeFormulae[testPkg], "cowsay should not be in before snapshot") - - // When: install cowsay via brew directly (simulates what openboot does) - installCmd := exec.Command("/opt/homebrew/bin/brew", "install", testPkg) - require.NoError(t, installCmd.Run(), "brew install cowsay should succeed") - t.Cleanup(func() { testutil.UninstallPackage(t, testPkg) }) - - // Capture after snapshot - after := captureSnapshot(t, binary) - afterFormulae := make(map[string]bool, len(after.Packages.Formulae)) - for _, f := range after.Packages.Formulae { - afterFormulae[f] = true - } - - // Then: cowsay should appear in after but not before - assert.True(t, afterFormulae[testPkg], "cowsay should be in after snapshot") - - // Verify only expected change: cowsay was added - added := []string{} - for _, f := range after.Packages.Formulae { - if !beforeFormulae[f] { - added = append(added, f) - } - } - assert.Contains(t, added, testPkg, "cowsay should be in added packages") -} - -func TestSmoke_DryRunNoSideEffects(t *testing.T) { - binary := testutil.BuildTestBinary(t) - - // Capture before snapshot - before := captureSnapshot(t, binary) - - // When: run with --dry-run --preset full - cmd := exec.Command(binary, "install", "--preset", "full", "--dry-run", "--silent") - cmd.Env = append(os.Environ(), - "OPENBOOT_GIT_NAME=Smoke Test", - "OPENBOOT_GIT_EMAIL=smoke@test.local", - ) - output, err := cmd.CombinedOutput() - t.Logf("Dry-run output: %s", string(output)) - assert.NoError(t, err, "dry-run should succeed") - - // Capture after snapshot - after := captureSnapshot(t, binary) - - // Then: formulae lists should be identical (use ElementsMatch for order-independence) - assert.ElementsMatch(t, before.Packages.Formulae, after.Packages.Formulae, - "dry-run should not change installed formulae") - assert.ElementsMatch(t, before.Packages.Casks, after.Packages.Casks, - "dry-run should not change installed casks") - assert.ElementsMatch(t, before.Packages.Npm, after.Packages.Npm, - "dry-run should not change installed npm packages") -} - From 2ba31b4d6fb6cc97fa9437f9df1cae2858926f90 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 09:36:18 +0800 Subject: [PATCH 10/18] =?UTF-8?q?ci:=20spike=20=E2=80=94=20run=20TestVM=5F?= =?UTF-8?q?Infra=20on=20macos-14=20GitHub=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/vm-e2e-spike.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/vm-e2e-spike.yml diff --git a/.github/workflows/vm-e2e-spike.yml b/.github/workflows/vm-e2e-spike.yml new file mode 100644 index 0000000..aec2417 --- /dev/null +++ b/.github/workflows/vm-e2e-spike.yml @@ -0,0 +1,24 @@ +name: vm-e2e-spike + +on: + push: + branches: ["test/vm-e2e-speed"] + workflow_dispatch: + +jobs: + infra-probe: + runs-on: macos-14 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build binary + run: make build + + - name: Run TestVM_Infra + run: go test -v -timeout 5m -tags="e2e,vm" -run TestVM_Infra ./test/e2e/... From 0585c5e97f44a08c22ef0e22872c6afcf0983932 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 09:40:33 +0800 Subject: [PATCH 11/18] =?UTF-8?q?ci:=20spike=20=E2=80=94=20full=20parallel?= =?UTF-8?q?=20vm-e2e=20on=20macos-14=20(group-a=20+=20group-b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/vm-e2e-spike.yml | 35 ++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/vm-e2e-spike.yml b/.github/workflows/vm-e2e-spike.yml index aec2417..0f03bee 100644 --- a/.github/workflows/vm-e2e-spike.yml +++ b/.github/workflows/vm-e2e-spike.yml @@ -6,9 +6,11 @@ on: workflow_dispatch: jobs: - infra-probe: + # Mirrors VM_A in make test-vm-parallel: + # long-running journey tests that install packages and modify system state. + group-a: runs-on: macos-14 - timeout-minutes: 10 + timeout-minutes: 60 steps: - uses: actions/checkout@v4 @@ -20,5 +22,30 @@ jobs: - name: Build binary run: make build - - name: Run TestVM_Infra - run: go test -v -timeout 5m -tags="e2e,vm" -run TestVM_Infra ./test/e2e/... + - name: Run group-a tests + run: | + go test -v -timeout 55m -tags="e2e,vm" \ + -run 'TestVM_Journey_FirstTimeUser|TestVM_Journey_DryRunIsCompletelySafe|TestVM_Interactive_InstallScript' \ + ./test/e2e/... + + # Mirrors VM_B in make test-vm-parallel: + # dotfiles, macOS defaults, edge cases, sync, and non-destructive e2e. + group-b: + runs-on: macos-14 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build binary + run: make build + + - name: Run group-b tests + run: | + go test -v -timeout 55m -tags="e2e,vm" \ + -run 'TestVM_Journey_Dotfiles|TestVM_Journey_MacOS|TestVM_Journey_FullSetupConfiguresEverything|TestVM_Edge_|TestSmoke_|TestE2E_' \ + ./test/e2e/... From c9453df6bdd80fbd95d59bf95a26fab7bcb6f830 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:02:03 +0800 Subject: [PATCH 12/18] fix: dry-run must not write packages cache; adapt vm e2e for pre-release CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: RefreshPackagesFromRemoteDryRun() skips writePackagesCache so --dry-run has zero side effects on ~/.openboot/ - root.go: call the dry-run variant when installCfg.DryRun is set - TestVM_Journey_FirstTimeUser: use dev binary (vmCopyDevBinary) instead of brew tap install — brew formula requires a published release - TestVM_Interactive_InstallScript: skip when openboot formula is not available in Homebrew (pre-release CI environment) --- internal/cli/root.go | 6 ++++- internal/config/packages_remote.go | 21 ++++++++++++---- internal/config/packages_remote_test.go | 4 ++-- test/e2e/vm_interactive_test.go | 12 ++++++++++ test/e2e/vm_user_journey_test.go | 32 ++++++++++++------------- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/internal/cli/root.go b/internal/cli/root.go index b2a4084..215d97f 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -55,7 +55,11 @@ shell configuration, and macOS preferences.`, // network overhead. if cmd.Name() == "install" { updater.AutoUpgrade(version) - config.RefreshPackagesFromRemote() + if installCfg.DryRun { + config.RefreshPackagesFromRemoteDryRun() + } else { + config.RefreshPackagesFromRemote() + } } return nil diff --git a/internal/config/packages_remote.go b/internal/config/packages_remote.go index 6d86765..177b45d 100644 --- a/internal/config/packages_remote.go +++ b/internal/config/packages_remote.go @@ -38,7 +38,18 @@ type packagesCacheEntry struct { // into the global Categories slice. Safe to call multiple times; it is a no-op // if the cache is fresh. Falls back to the embedded packages.yaml silently. func RefreshPackagesFromRemote() { - pkgs, err := loadRemotePackages() + refreshPackages(false) +} + +// RefreshPackagesFromRemoteDryRun is identical to RefreshPackagesFromRemote +// but suppresses writing the on-disk cache. Use during --dry-run so the +// command has zero side effects on ~/.openboot/. +func RefreshPackagesFromRemoteDryRun() { + refreshPackages(true) +} + +func refreshPackages(dryRun bool) { + pkgs, err := loadRemotePackages(dryRun) if err != nil || len(pkgs) == 0 { return // keep embedded fallback } @@ -47,7 +58,7 @@ func RefreshPackagesFromRemote() { mergeRemotePackages(pkgs) } -func loadRemotePackages() ([]remotePackage, error) { +func loadRemotePackages(dryRun bool) ([]remotePackage, error) { // Try disk cache first. if pkgs, err := readPackagesCache(); err == nil { return pkgs, nil @@ -59,8 +70,10 @@ func loadRemotePackages() ([]remotePackage, error) { return nil, err } - // Write cache (best-effort). - _ = writePackagesCache(pkgs) + // Write cache (best-effort) — skip during dry-run to avoid disk side effects. + if !dryRun { + _ = writePackagesCache(pkgs) + } return pkgs, nil } diff --git a/internal/config/packages_remote_test.go b/internal/config/packages_remote_test.go index 0e33458..61f440f 100644 --- a/internal/config/packages_remote_test.go +++ b/internal/config/packages_remote_test.go @@ -291,7 +291,7 @@ func TestLoadRemotePackages_UsesCacheThenFallsToNetwork(t *testing.T) { // No cache, no server → error. t.Setenv("OPENBOOT_API_URL", "http://localhost:1") - _, err := loadRemotePackages() + _, err := loadRemotePackages(false) assert.Error(t, err) // Write fresh cache. @@ -303,7 +303,7 @@ func TestLoadRemotePackages_UsesCacheThenFallsToNetwork(t *testing.T) { os.WriteFile(filepath.Join(dir, packagesCacheFile), data, 0600) // Cache hit → no network call needed. - pkgs, err := loadRemotePackages() + pkgs, err := loadRemotePackages(false) require.NoError(t, err) assert.Equal(t, "cached-pkg", pkgs[0].Name) } diff --git a/test/e2e/vm_interactive_test.go b/test/e2e/vm_interactive_test.go index 3c48cd7..a81c77e 100644 --- a/test/e2e/vm_interactive_test.go +++ b/test/e2e/vm_interactive_test.go @@ -20,6 +20,18 @@ func TestVM_Interactive_InstallScript(t *testing.T) { } vm := testutil.NewMacHost(t) + + // This test exercises install.sh's reinstall prompt, which requires openboot + // to be registered with Homebrew (`brew list openboot`). Skip if the formula + // isn't available — that means we're in a pre-release CI environment where + // the tap has no published formula yet. + tapScript := fmt.Sprintf( + "export PATH=%q && brew tap openbootdotdev/openboot 2>/dev/null; brew info openboot &>/dev/null", + brewPath, + ) + if _, err := vm.Run(tapScript); err != nil { + t.Skip("openboot Homebrew formula not available — skipping brew-dependent interactive test") + } vmInstallViaBrew(t, vm) // Install first (no TTY required) // expect is required for interactive tests. diff --git a/test/e2e/vm_user_journey_test.go b/test/e2e/vm_user_journey_test.go index fe3e350..21cae6c 100644 --- a/test/e2e/vm_user_journey_test.go +++ b/test/e2e/vm_user_journey_test.go @@ -40,9 +40,8 @@ func TestVM_Journey_FirstTimeUser(t *testing.T) { } vm := testutil.NewMacHost(t) - - // Clean up any openboot left over from a prior test run. - vm.Run(fmt.Sprintf("export PATH=%q && brew uninstall openboot 2>/dev/null || true", brewPath)) //nolint:errcheck // best-effort cleanup + vmInstallHomebrew(t, vm) + bin := vmCopyDevBinary(t, vm) // Step 1: openboot shouldn't leak in from a prior step. t.Run("bare_system_has_no_openboot", func(t *testing.T) { @@ -50,28 +49,29 @@ func TestVM_Journey_FirstTimeUser(t *testing.T) { assert.Contains(t, out, "not-found", "openboot should not exist before install") }) - // Step 2: Install via brew tap (no TTY required; mirrors the curl|bash tap path). - t.Run("installs_via_brew_tap", func(t *testing.T) { - version := vmInstallViaBrew(t, vm) - assert.Contains(t, version, "OpenBoot v", "should report version after install") + // Step 2: Dev binary runs and reports a version. + t.Run("binary_is_available", func(t *testing.T) { + out, err := vm.Run(bin + " version") + require.NoError(t, err) + assert.Contains(t, out, "OpenBoot", "binary should report version") }) // Step 3: Run openboot with minimal preset t.Run("minimal_preset_installs_usable_tools", func(t *testing.T) { - output, err := vmRunOpenbootWithGit(t, vm, "install --preset minimal --silent --packages-only") + output, err := vmRunDevBinaryWithGit(t, vm, bin, "install --preset minimal --silent --packages-only") t.Logf("install output:\n%s", output) require.NoError(t, err, "minimal preset should succeed") // User expectation: every tool should be USABLE, not just "in PATH" toolChecks := map[string]string{ - "jq": `echo '{"a":1}' | jq '.a'`, // Can it parse JSON? - "rg": `echo 'hello world' | rg 'hello'`, // Can it search? - "fd": `fd --version`, // Does it run? - "bat": `echo 'test' | bat --plain`, // Can it display? - "fzf": `echo 'a\nb\nc' | fzf --filter 'b'`, // Can it filter? - "htop": `htop --version`, // Does it run? - "tree": `tree --version`, // Does it run? - "gh": `gh --version`, // Does it run? + "jq": `echo '{"a":1}' | jq '.a'`, + "rg": `echo 'hello world' | rg 'hello'`, + "fd": `fd --version`, + "bat": `echo 'test' | bat --plain`, + "fzf": `echo 'a\nb\nc' | fzf --filter 'b'`, + "htop": `htop --version`, + "tree": `tree --version`, + "gh": `gh --version`, } for name, cmd := range toolChecks { From 38163851091ef2d4a171b244156e7be2b7d534c6 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:03:22 +0800 Subject: [PATCH 13/18] fix: remove unused fmt import from vm_user_journey_test --- test/e2e/vm_user_journey_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/vm_user_journey_test.go b/test/e2e/vm_user_journey_test.go index 21cae6c..2141225 100644 --- a/test/e2e/vm_user_journey_test.go +++ b/test/e2e/vm_user_journey_test.go @@ -14,7 +14,6 @@ package e2e import ( - "fmt" "strings" "testing" From 719de10d951eebbf5f0fee8875d3209f01bfd4c2 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:09:50 +0800 Subject: [PATCH 14/18] chore: remove tart-based test-vm targets; L4 now runs on github actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-vm, test-vm-run, test-vm-parallel and their VM_A/VM_B variables are deleted. L4 destructive e2e now runs on macos-14 GitHub Actions runners (vm-e2e-spike.yml) — each job gets a fresh macOS VM at no extra cost for public repos. Kept test-vm-inner / test-vm-inner-run for local use and as the commands the CI workflow invokes. Updated: Makefile, CLAUDE.md, CONTRIBUTING.md, AGENTS.md, HARNESS.md, auto-release.yml checklist. --- .github/workflows/auto-release.yml | 6 ++--- .github/workflows/vm-e2e-spike.yml | 6 ++--- AGENTS.md | 4 +-- CLAUDE.md | 4 +-- CONTRIBUTING.md | 27 ++++++-------------- Makefile | 40 ++++++------------------------ docs/HARNESS.md | 17 ++++++------- 7 files changed, 31 insertions(+), 73 deletions(-) diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 309ae5e..f04e89e 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -176,13 +176,13 @@ jobs: **not** auto-tag. Run the destructive e2e suite locally, then cut the release manually: - - [ ] \`make test-vm-parallel\` passes (Apple Silicon + Tart required — see scripts/vm/README.md) + - [ ] L4 CI (\`vm-e2e-spike.yml\`) is green on the latest commit on \`main\` - [ ] sanity-check the curl|bash smoke and cli-compat results in the most recent test.yml run on main - [ ] \`git tag -a ${NEW_TAG} -m "..."\` and \`git push origin ${NEW_TAG}\` - [ ] close this issue - Skipping \`make test-vm-parallel\` is allowed (it is not a hard gate), - but \`feat:\` changes carry more risk than \`fix:\` patches. + L4 CI is not yet a hard merge gate, but \`feat:\` changes carry + more risk than \`fix:\` patches — verify it before tagging. EOF ) diff --git a/.github/workflows/vm-e2e-spike.yml b/.github/workflows/vm-e2e-spike.yml index 0f03bee..7ebe857 100644 --- a/.github/workflows/vm-e2e-spike.yml +++ b/.github/workflows/vm-e2e-spike.yml @@ -6,8 +6,7 @@ on: workflow_dispatch: jobs: - # Mirrors VM_A in make test-vm-parallel: - # long-running journey tests that install packages and modify system state. + # Group A: long-running journey tests that install packages and modify system state. group-a: runs-on: macos-14 timeout-minutes: 60 @@ -28,8 +27,7 @@ jobs: -run 'TestVM_Journey_FirstTimeUser|TestVM_Journey_DryRunIsCompletelySafe|TestVM_Interactive_InstallScript' \ ./test/e2e/... - # Mirrors VM_B in make test-vm-parallel: - # dotfiles, macOS defaults, edge cases, sync, and non-destructive e2e. + # Group B: dotfiles, macOS defaults, edge cases, sync, and non-destructive e2e. group-b: runs-on: macos-14 timeout-minutes: 60 diff --git a/AGENTS.md b/AGENTS.md index 21e3d7f..6a45807 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,8 +81,8 @@ These are loaded automatically when Claude runs in this repo. - `git push --force` against `main` or release tags. - `git commit --amend` on commits already pushed. - `git reset --hard` discarding uncommitted work. -- Running `make test-vm` (or any other `test-vm-*` target) outside an ephemeral - VM — these install real packages. +- Running `make test-vm-inner` (or `test-vm-inner-run`) outside a throwaway + machine — these install real packages onto the current host. - Anything that modifies the user's `~/.zshrc`, Homebrew install, or macOS `defaults`. diff --git a/CLAUDE.md b/CLAUDE.md index 95d5d29..e0e0a21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,8 +24,8 @@ make build-release VERSION=0.25.0 # optimized + UPX # Test — full tier table in CONTRIBUTING.md make test-unit # L1 (~75s) — unit + integration + contract; pre-push hook make test-e2e # L3 compiled binary -make test-vm-parallel # L4 (~14 min) — destructive e2e in 2 parallel Tart VMs; before tagging -make test-vm # L4 serial fallback — single VM, use for debugging +make test-vm-inner # L4 — full destructive e2e suite (runs in CI on macos-14; locally on a spare machine only) +make test-vm-inner-run TEST=Foo # L4 — single test make test-coverage # coverage.out + coverage.html # Single test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9f0b12..2bb150c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,37 +37,24 @@ Tests are split across four tiers. Which one runs where: | **L1 Unit + Integration + Contract** | Pure-Go logic with faked `Runner` *plus* real `brew` / `git` / `npm` against temp dirs and real `httptest` servers | `make test-unit` (~75s) | Every push (pre-push hook); CI on push/PR | | **L2 Contract schema** | JSON schema validation against [openboot-contract](https://github.com/openbootdotdev/openboot-contract) | (runs in CI only) | CI on push/PR | | **L3 E2E binary** | Compiled binary driven by scripts; `-tags=e2e` | `make test-e2e` | CI on release | -| **L4 VM e2e** | Full destructive suite (`-tags="e2e,vm"`) runs inside an ephemeral Tart VM provisioned by `scripts/vm/run.sh`. Installs real packages, modifies `~/.zshrc`, writes `defaults` — all contained to the throwaway VM. | `make test-vm-parallel` (~14 min, Apple Silicon + Tart required); `make test-vm` is the serial single-VM fallback | **Local only** — convention is to run before tagging a release. No CI gate. | +| **L4 VM e2e** | Full destructive suite (`-tags="e2e,vm"`). Installs real packages, modifies `~/.zshrc`, writes `defaults`. Each run requires a clean macOS host (Apple Silicon). | `make test-vm-inner` (or single test: `make test-vm-inner-run TEST=Foo`) | **CI** — runs on GitHub Actions `macos-14` runner (every PR via `vm-e2e-spike.yml`). Locally only on a throwaway machine. | Rules of thumb: - **Local dev:** run nothing manually if hooks are installed. `make test-unit` on demand when you want a sanity check. Skip L2+ unless you're cutting a release. - **Before pushing:** `make test-unit` (the pre-push hook does this automatically). Requires `brew` / `git` / `npm` on PATH — they are queried read-only against temp dirs, no real installs. -- **Before tagging a release (convention, not enforced):** `make test-vm-parallel` on an Apple Silicon Mac with Tart installed. See [VM E2E setup](#vm-e2e-setup) below. `auto-release.yml` opens a `release-ready` issue on `feat:` thresholds to nudge you here. +- **Before tagging a release:** check that the L4 CI job (`vm-e2e-spike.yml`) is green on the latest commit on `main`. `auto-release.yml` opens a `release-ready` issue on `feat:` thresholds to nudge you here. -## VM E2E setup +## VM E2E -Destructive tests (L4) run inside an ephemeral Tart VM. One-time setup -on an Apple Silicon Mac: +L4 tests run on GitHub Actions (`macos-14` runner, Apple Silicon). Each job +gets a fresh macOS VM — no local setup required. ```bash -brew install cirruslabs/cli/tart -tart pull ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest -tart clone ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest tahoe-base +make test-vm-inner # full suite (use on a throwaway machine only) +make test-vm-inner-run TEST=TestVM_Journey_FirstTimeUser # single test ``` -Then: - -```bash -make test-vm-parallel # full suite in parallel (~14 min) -make test-vm # full suite serial fallback (~30 min) -make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test -OPENBOOT_VM_KEEP=1 make test-vm-parallel # don't destroy VMs at exit (debug) -``` - -See `scripts/vm/README.md` for full environment-variable docs and -troubleshooting. - ## Git Hooks `make install-hooks` symlinks two hooks from `scripts/hooks/` into `.git/hooks/`: diff --git a/Makefile b/Makefile index 8b5bccd..68adf26 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,7 @@ .PHONY: test-unit test-e2e test-coverage test-all \ - test-vm test-vm-run test-vm-parallel test-vm-inner test-vm-inner-run \ + test-vm-inner test-vm-inner-run \ install-hooks uninstall-hooks -# VM A: install/journey tests that touch real system state (longest-running). -VM_A_TESTS := TestVM_Journey_FirstTimeUser|TestVM_Journey_DryRunIsCompletelySafe|TestVM_Interactive_InstallScript -# VM B: all other VM tests — dotfiles, macOS, edge cases, smoke, real-install, sync. -VM_B_TESTS := TestVM_Journey_Dotfiles|TestVM_Journey_MacOS|TestVM_Journey_FullSetupConfiguresEverything|TestVM_Edge_|TestSmoke_|TestE2E_ - BINARY_NAME=openboot BINARY_PATH=./$(BINARY_NAME) VERSION ?= dev @@ -34,38 +29,17 @@ test-all: $(MAKE) test-coverage # ============================================================================= -# Tart VM e2e — destructive tests run inside a throwaway Tart VM provisioned -# by scripts/vm/run.sh. Files tagged `e2e,vm` run via `make test-vm-inner`; -# files tagged `e2e && !vm` (auth, snapshot_api) run as L3 on the host. -# -# Requires Apple Silicon + Tart installed locally. See scripts/vm/README.md -# for one-time setup. The relevant targets are defined immediately below -# this header: test-vm, test-vm-run, test-vm-inner, test-vm-inner-run. +# L4 VM e2e — destructive tests tagged `e2e,vm`. Run directly on any clean +# macOS host (Apple Silicon). In CI this is a GitHub Actions macos-14 runner +# (see .github/workflows/vm-e2e-spike.yml). Locally, run on a throwaway +# machine or a Tart VM — do NOT run on your primary dev machine. # ============================================================================= -# Developer-facing: provisions a Tart VM and runs the full e2e suite inside. -test-vm: build - scripts/vm/run.sh test-vm-inner - -# Developer-facing: runs one named test inside a Tart VM. -test-vm-run: build - scripts/vm/run.sh "test-vm-inner-run TEST=$(TEST)" - -# Developer-facing: runs e2e in two parallel VMs — VM A (system tests) and -# VM B (mock-server tests). Requires ~16 GB RAM and 8 cores free. -# Exit code is non-zero if either VM fails. -test-vm-parallel: build - @OPENBOOT_VM_TEST='$(VM_A_TESTS)' scripts/vm/run.sh test-vm-inner & PID_A=$$!; \ - OPENBOOT_VM_TEST='$(VM_B_TESTS)' scripts/vm/run.sh test-vm-inner & PID_B=$$!; \ - A_EXIT=0; B_EXIT=0; \ - wait $$PID_A || A_EXIT=$$?; \ - wait $$PID_B || B_EXIT=$$?; \ - [ $$A_EXIT -eq 0 ] && [ $$B_EXIT -eq 0 ] - -# In-VM: invoked over SSH by run.sh — not called by developers directly. +# Run the full L4 suite (same command CI uses). test-vm-inner: go test -v -timeout 60m -tags="e2e,vm" ./test/e2e/... +# Run a single L4 test by name: make test-vm-inner-run TEST=TestVM_Journey_FirstTimeUser test-vm-inner-run: go test -v -timeout 45m -tags="e2e,vm" -run '$(TEST)' ./test/e2e/... diff --git a/docs/HARNESS.md b/docs/HARNESS.md index a9ca46c..e96828e 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -48,9 +48,9 @@ Three regulation categories: | Behav. | L1 unit + integration + contract (faked runners *and* real brew/git/npm in temp dirs) | pre-push, CI | `make test-unit` | | Behav. | L2 contract schema (against openboot-contract repo) | CI | `.github/workflows/test.yml` `contract` job | | Behav. | L3 e2e binary | release | `make test-e2e` | -| Behav. | L4 VM e2e (`vm`) — runs full destructive suite in a local Tart VM | local only (convention is pre-release; no CI gate) | `make test-vm-parallel` (~14 min, driver: `scripts/vm/run.sh`); `make test-vm` is the serial single-VM fallback | +| Behav. | L4 VM e2e (`vm`) — full destructive suite on a clean macOS host | every PR | `.github/workflows/vm-e2e-spike.yml` (macos-14 runner, two parallel jobs); `make test-vm-inner` for local runs | | Behav. | curl\|bash smoke (install.sh + mock server) | every PR | `.github/workflows/test.yml` `curl-bash-smoke` job | -| Behav. | Auto-release sensor — patch fast lane (`fix:`-only) auto-tags + dispatches `release.yml`; feat threshold opens a `release-ready` issue with a `make test-vm-parallel` checklist instead | push to `main` | `.github/workflows/auto-release.yml` | +| Behav. | Auto-release sensor — patch fast lane (`fix:`-only) auto-tags + dispatches `release.yml`; feat threshold opens a `release-ready` issue (check L4 CI green, then tag manually) | push to `main` | `.github/workflows/auto-release.yml` | | Behav. | Release notes — Conventional Commits since previous tag, grouped by type (Features / Bug Fixes / etc) + Full Changelog link, appended to the install-instructions template | tag push or `workflow_dispatch` | `.github/workflows/release.yml` (`Write release notes` step) | | Behav. | Old-CLI compat (previous release × current mock server) | every PR | `.github/workflows/test.yml` `cli-compat` job | | Feedfwd. | Agent conventions | every AI turn | `CLAUDE.md`, `AGENTS.md` | @@ -110,13 +110,12 @@ it survives doc rot. to the inline `\r\033[K` renderer when unavailable. A static rule can't see runtime terminal capabilities, so this stays a runtime concern. The fallback is covered by `TestStickyProgressFallsBackWhenScrollRegionUnsupported`. -- **No CI gate for VM e2e.** Apple Silicon Tart VMs don't run on - GitHub-hosted `macos-latest` runners (no nested virt, wrong arch - guarantees), and we declined to set up a self-hosted runner. L4 is - local-only. Running `make test-vm-parallel` before tagging is convention, - encoded as a `release-ready` issue opened by `auto-release.yml` - on `feat:` thresholds — not a hard gate. A human can release without - it. +- **L4 runs on GitHub Actions, not a self-hosted runner.** `macos-14` + runners are Apple Silicon VMs — each job gets a fresh clean macOS + environment, which is exactly what L4 needs. Tart is no longer required. + The L4 workflow (`vm-e2e-spike.yml`) is not yet a hard merge gate (not in + `required-checks.txt`); it runs on every PR. Promoting it to a required + check is the next step once the workflow has proven stable. ## How agents should think about this file From 793b72b054b1fc74463de06fbe644a8ee45b6028 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:11:45 +0800 Subject: [PATCH 15/18] chore: delete scripts/vm/ and remove stale run.sh references --- scripts/vm/README.md | 81 ------------------------------------- scripts/vm/lib.sh | 59 --------------------------- scripts/vm/run.sh | 96 -------------------------------------------- testutil/machost.go | 15 +++---- 4 files changed, 6 insertions(+), 245 deletions(-) delete mode 100644 scripts/vm/README.md delete mode 100755 scripts/vm/lib.sh delete mode 100755 scripts/vm/run.sh diff --git a/scripts/vm/README.md b/scripts/vm/README.md deleted file mode 100644 index e1c7f46..0000000 --- a/scripts/vm/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Tart VM e2e - -The `scripts/vm/run.sh` driver spins up an ephemeral Tart VM for the -destructive e2e suite (`make test-vm-parallel`). The 12 test files in `test/e2e/` -run inside it. - -## One-time setup - -Tart only runs on Apple Silicon Macs. - -```bash -# 1. Install Tart (https://tart.run) -brew install cirruslabs/cli/tart - -# 2. Pull the base image (downloads ~25GB) -# Canonical upstream: -tart pull ghcr.io/cirruslabs/tahoe-base:latest -# Faster mirror for users in China: -# tart pull ghcr.nju.edu.cn/cirruslabs/tahoe-base:latest - -# 3. Give it the local name run.sh expects -tart clone ghcr.io/cirruslabs/tahoe-base:latest tahoe-base -``` - -Verify with `tart list` — you should see `tahoe-base`. - -## Running - -```bash -make test-vm-parallel # full suite (~14 min, 2 parallel VMs) — use before tagging -make test-vm # serial fallback — single VM, useful for debugging -make test-vm-run TEST=TestVM_Journey_FirstTimeUser # one test -``` - -## Environment variables - -| Var | Default | Effect | -|---|---|---| -| `OPENBOOT_VM_BASE` | `tahoe-base` | Local Tart image to clone from | -| `OPENBOOT_VM_KEEP` | `0` | When `1`, do not destroy the VM at exit. Useful for debugging — attach with the SSH key path printed at exit. Remember to clean up manually. | - -## Troubleshooting - -- **`base image 'tahoe-base' not found locally`** — run the one-time - setup above. -- **SSH not reachable within 60s** — Tart logs dumped to stderr. - Try `OPENBOOT_VM_KEEP=1 make test-vm` (it'll still fail at boot, but the - VM is left running so you can `tart ssh openboot-ephemeral-` and - see what's up). -- **Leaked VMs after a hard kill** — just run `make test-vm` again; run.sh - sweeps all `openboot-ephemeral-*` VMs on startup automatically. If you - want to inspect a leaked VM before deleting it, stop and delete it manually: - - ```bash - tart stop openboot-ephemeral- - tart delete openboot-ephemeral- - ``` - -- **Base image needs updating** — re-pull and re-clone: - - ```bash - tart delete tahoe-base - tart pull ghcr.io/cirruslabs/tahoe-base:latest - tart clone ghcr.io/cirruslabs/tahoe-base:latest tahoe-base - ``` - -## Why a base image (not vanilla) - -`-base` includes Xcode CLI tools and Homebrew pre-installed. `run.sh` -installs Go on first boot via `mise` (which the base image ships). openboot -*does* install Homebrew, so "fresh Mac" purists would prefer -`macos-tahoe-vanilla`. We pick `-base` because: - -1. Boot-to-test latency drops significantly vs. having to bootstrap brew - and Go from scratch. -2. `MacHost.Run("brew install ...")` exercises the "already-installed - brew" code path, which is the more common real-world scenario (users - running openboot a second time, or after `brew install` ran for some - other reason). -3. Tests that specifically need to exercise the "no brew yet" path can - uninstall it as the first step. diff --git a/scripts/vm/lib.sh b/scripts/vm/lib.sh deleted file mode 100755 index 27b322c..0000000 --- a/scripts/vm/lib.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Shared helpers for scripts/vm/run.sh. - -die() { - printf 'scripts/vm: %s\n' "$*" >&2 - exit 1 -} - -# wait_for_ssh — poll `tart ip` until SSH on port 22 is reachable. -# Echoes the VM's IP to stdout on success; dies on 60s timeout. -wait_for_ssh() { - local vm="$1" - local deadline=$(( $(date +%s) + 60 )) - local ip="" - while [ "$(date +%s)" -lt "$deadline" ]; do - ip=$(tart ip "$vm" 2>/dev/null || true) - if [ -n "$ip" ]; then - if nc -z -G 2 "$ip" 22 2>/dev/null; then - printf '%s' "$ip" - return 0 - fi - fi - sleep 1 - done - printf 'scripts/vm: SSH not reachable on %s within 60s\n' "$vm" >&2 - printf '----- tart logs %s -----\n' "$vm" >&2 - tart logs "$vm" 2>&1 | tail -50 >&2 || true - exit 1 -} - -# ssh_exec — run a shell command in the VM as admin. -# Uses a fixed -o pair so first-run host-key prompts don't hang the script. -# IdentitiesOnly=yes prevents local SSH agents from flooding MaxAuthTries. -ssh_exec() { - local ip="$1" - local key="$2" - local cmd="$3" - ssh \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o LogLevel=ERROR \ - -o IdentitiesOnly=yes \ - -i "$key" \ - "admin@${ip}" \ - "$cmd" -} - -# sweep_leaked_vms — delete any openboot-ephemeral-* VMs left over from a -# previous run that was killed before its EXIT trap could fire. -sweep_leaked_vms() { - local leaked - while IFS= read -r leaked; do - [ -z "$leaked" ] && continue - printf 'scripts/vm: cleaning leaked VM: %s\n' "$leaked" >&2 - tart stop "$leaked" 2>/dev/null || true - tart delete "$leaked" 2>/dev/null || true - done < <(tart list --format=json 2>/dev/null \ - | python3 -c 'import json,sys; [print(v["Name"]) for v in json.load(sys.stdin) if v["Name"].startswith("openboot-ephemeral-")]' 2>/dev/null || true) -} diff --git a/scripts/vm/run.sh b/scripts/vm/run.sh deleted file mode 100755 index 614181a..0000000 --- a/scripts/vm/run.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# Run an in-VM make target inside an ephemeral Tart VM. -# -# Usage: -# scripts/vm/run.sh -# -# Env vars: -# OPENBOOT_VM_BASE — local Tart image name to clone (default: tahoe-base) -# OPENBOOT_VM_KEEP — if "1", do not destroy the VM at exit (for debugging) -# -# Exit codes match the in-VM make target. - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=lib.sh -. "$SCRIPT_DIR/lib.sh" - -[ $# -ge 1 ] || die "usage: $0 " -TARGET="$1" - -BASE="${OPENBOOT_VM_BASE:-tahoe-base}" -KEEP="${OPENBOOT_VM_KEEP:-0}" -VM_TEST="${OPENBOOT_VM_TEST:-}" -VM="openboot-ephemeral-$$" - -# Pre-flight — all checks before we create any disk state. -command -v tart >/dev/null 2>&1 || die "tart not installed — brew install cirruslabs/cli/tart" -command -v rsync >/dev/null 2>&1 || die "rsync not installed" -command -v ssh >/dev/null 2>&1 || die "ssh not installed" - -tart list --format=json 2>/dev/null \ - | python3 -c "import json,sys; sys.exit(0 if any(v['Name']=='$BASE' for v in json.load(sys.stdin)) else 1)" \ - || die "base image '$BASE' not found locally — see scripts/vm/README.md for one-time setup" - -sweep_leaked_vms - -# Ephemeral SSH key — injected into the VM via tart exec after boot. -# Created AFTER pre-flight so a failed check doesn't leave a private key on disk. -# Avoids interference from local SSH agents (1Password, gpg-agent, etc.) -# that exhaust the server's MaxAuthTries before a real key is tried. -SSH_KEY_DIR="$(mktemp -d)" -SSH_KEY="${SSH_KEY_DIR}/id_ed25519" -ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -q - -cleanup() { - rm -rf "$SSH_KEY_DIR" 2>/dev/null || true - if [ -n "$VM" ] && [ "$KEEP" = "1" ]; then - printf 'scripts/vm: OPENBOOT_VM_KEEP=1 — leaving "%s" running for debug\n' "$VM" >&2 - printf 'scripts/vm: tart ssh %s # attach (admin/admin)\n' "$VM" >&2 - printf 'scripts/vm: tart stop %s && tart delete %s # clean up when done\n' "$VM" "$VM" >&2 - return - fi - [ -n "$VM" ] && tart stop "$VM" 2>/dev/null || true - [ -n "$VM" ] && tart delete "$VM" 2>/dev/null || true -} -trap cleanup EXIT - -# Clone VM (trap is now registered — cleanup will fire on any subsequent die()) -tart clone "$BASE" "$VM" - -# Boot -tart run --no-graphics "$VM" >/dev/null 2>&1 & -VM_IP=$(wait_for_ssh "$VM") -printf 'scripts/vm: VM "%s" at %s ready\n' "$VM" "$VM_IP" >&2 - -# Inject ephemeral SSH key via tart exec (works without prior SSH auth setup) -PUBKEY="$(cat "${SSH_KEY}.pub")" -tart exec "$VM" mkdir -p /Users/admin/.ssh -tart exec "$VM" sh -c "printf '%s\n' '$PUBKEY' >> /Users/admin/.ssh/authorized_keys" -tart exec "$VM" chmod 700 /Users/admin/.ssh -tart exec "$VM" chmod 600 /Users/admin/.ssh/authorized_keys - -# Ensure Go is available via mise (the tahoe-base image ships mise but -# not Go pre-activated in the default shell PATH). Both commands are idempotent. -tart exec "$VM" sh -c '/opt/homebrew/bin/mise install go@latest && /opt/homebrew/bin/mise use -g go@latest' - -# Sync source -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -rsync -az --delete \ - --exclude='/.git/objects' \ - --exclude='/coverage.out' \ - --exclude='/coverage.html' \ - -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o IdentitiesOnly=yes -i $SSH_KEY" \ - "$REPO_ROOT/" "admin@${VM_IP}:/Users/admin/openboot/" - -# Execute target inside VM — use mise exec to put Go on PATH. -# When OPENBOOT_VM_TEST is set, run only the matching tests; single-quote the -# value so that regexp metacharacters (| [ ]) survive the remote shell intact. -if [ -n "$VM_TEST" ]; then - MAKE_CMD="test-vm-inner-run TEST='${VM_TEST}'" -else - MAKE_CMD="${TARGET}" -fi -ssh_exec "$VM_IP" "$SSH_KEY" \ - "cd /Users/admin/openboot && CI=true OPENBOOT_IN_VM=1 /opt/homebrew/bin/mise exec go -- make ${MAKE_CMD}" diff --git a/testutil/machost.go b/testutil/machost.go index 82bf8a4..f2fe296 100644 --- a/testutil/machost.go +++ b/testutil/machost.go @@ -1,15 +1,12 @@ //go:build e2e && vm // MacHost runs destructive openboot E2E tests directly against the -// current macOS host — typically an ephemeral Tart VM provisioned by -// scripts/vm/run.sh. The driver SSHs in, rsyncs the source, and invokes -// `make test-vm-inner`; MacHost then executes commands against the VM -// it's already running inside. +// current macOS host. In CI this is a fresh GitHub Actions macos-14 runner; +// locally it should only be used on a throwaway machine. // -// A host refuses to activate unless OPENBOOT_IN_VM=1 (set by run.sh) or -// the legacy CI=true / OPENBOOT_E2E_DESTRUCTIVE=1 envs are set — so -// `go test -tags="e2e,vm"` on a bare developer machine is a no-op -// rather than a foot-gun. +// A host refuses to activate unless CI=true, OPENBOOT_IN_VM=1, or +// OPENBOOT_E2E_DESTRUCTIVE=1 is set — so `go test -tags="e2e,vm"` on a +// developer's primary machine is a no-op rather than a foot-gun. package testutil @@ -120,7 +117,7 @@ func requireEphemeralHost(t *testing.T) { if os.Getenv("CI") == "true" || os.Getenv("OPENBOOT_E2E_DESTRUCTIVE") == "1" { return } - t.Skip("destructive macOS E2E tests require running inside scripts/vm/run.sh (or set OPENBOOT_E2E_DESTRUCTIVE=1)") + t.Skip("destructive macOS E2E tests require CI=true, OPENBOOT_IN_VM=1, or OPENBOOT_E2E_DESTRUCTIVE=1") } func requireMacOS(t *testing.T) { From db7d890c2fbc4a43831007d4ccd1440479c277c7 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:15:34 +0800 Subject: [PATCH 16/18] chore: update archtest no-raw-http baseline after loadRemotePackages refactor Line numbers shifted from :69/:74 to :82/:87 when loadRemotePackages gained the dryRun bool parameter and refreshPackages helper was added. --- internal/archtest/baseline/no-raw-http.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/archtest/baseline/no-raw-http.txt b/internal/archtest/baseline/no-raw-http.txt index 3d1a936..8c65f9b 100644 --- a/internal/archtest/baseline/no-raw-http.txt +++ b/internal/archtest/baseline/no-raw-http.txt @@ -4,8 +4,8 @@ internal/auth/login.go:106 internal/cli/snapshot_import.go:70 internal/cli/snapshot_publish.go:142 -internal/config/packages_remote.go:69 -internal/config/packages_remote.go:74 +internal/config/packages_remote.go:82 +internal/config/packages_remote.go:87 internal/config/remote.go:56 internal/config/remote.go:75 internal/config/remote.go:233 From 0587f57910555b13b68108df48106c07b324f776 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:21:21 +0800 Subject: [PATCH 17/18] style: remove stray blank line in macos_defaults_e2e_test.go --- test/e2e/macos_defaults_e2e_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/macos_defaults_e2e_test.go b/test/e2e/macos_defaults_e2e_test.go index bfdddbf..0115a90 100644 --- a/test/e2e/macos_defaults_e2e_test.go +++ b/test/e2e/macos_defaults_e2e_test.go @@ -22,7 +22,6 @@ import ( "github.com/openbootdotdev/openboot/testutil" ) - // macOSPrefCheck describes a single `defaults read` assertion. type macOSPrefCheck struct { domain string From 98508d32616deaa68f0f96588e0b2abbb8e974e9 Mon Sep 17 00:00:00 2001 From: fullstackjam Date: Tue, 19 May 2026 10:23:33 +0800 Subject: [PATCH 18/18] refactor: remove test-e2e-fixture preset; use minimal in e2e tests The preset was an internal fixture not intended for user consumption. Tests that previously used it now use the minimal preset, which is already tested by TestVM_Journey_FirstTimeUser in the same job. --- internal/config/data/presets.yaml | 9 --------- test/e2e/macos_defaults_e2e_test.go | 2 +- test/e2e/vm_edge_cases_test.go | 2 +- test/e2e/vm_user_journey_test.go | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/internal/config/data/presets.yaml b/internal/config/data/presets.yaml index 0f867c5..e13762a 100644 --- a/internal/config/data/presets.yaml +++ b/internal/config/data/presets.yaml @@ -29,15 +29,6 @@ presets: - stats - rectangle - test-e2e-fixture: - name: test-e2e-fixture - description: "Minimal fixture for VM e2e tests — not for real use" - cli: - - jq - - bat - - ripgrep - cask: [] - developer: name: developer description: Ready-to-code Mac setup (Node, Go, Docker, VS Code) diff --git a/test/e2e/macos_defaults_e2e_test.go b/test/e2e/macos_defaults_e2e_test.go index 0115a90..675185b 100644 --- a/test/e2e/macos_defaults_e2e_test.go +++ b/test/e2e/macos_defaults_e2e_test.go @@ -49,7 +49,7 @@ func TestVM_Journey_MacOSDefaults_AllCategoriesWritten(t *testing.T) { bin := vmCopyDevBinary(t, vm) output, err := vmRunDevBinaryWithGit(t, vm, bin, - "install --preset test-e2e-fixture --silent --shell skip --dotfiles skip --macos configure") + "install --preset minimal --silent --shell skip --dotfiles skip --macos configure") t.Logf("macOS configure output:\n%s", output) require.NoError(t, err, "install with --macos configure should succeed") diff --git a/test/e2e/vm_edge_cases_test.go b/test/e2e/vm_edge_cases_test.go index ac34fee..594231b 100644 --- a/test/e2e/vm_edge_cases_test.go +++ b/test/e2e/vm_edge_cases_test.go @@ -33,7 +33,7 @@ func TestVM_Edge_ShellActuallyWorks(t *testing.T) { bin := vmCopyDevBinary(t, vm) // Install with shell setup - _, err := vmRunDevBinaryWithGit(t, vm, bin, "install --preset test-e2e-fixture --silent --shell install --dotfiles skip --macos skip") + _, err := vmRunDevBinaryWithGit(t, vm, bin, "install --preset minimal --silent --shell install --dotfiles skip --macos skip") require.NoError(t, err) t.Run("zsh_login_shell_starts", func(t *testing.T) { diff --git a/test/e2e/vm_user_journey_test.go b/test/e2e/vm_user_journey_test.go index 2141225..5288dbe 100644 --- a/test/e2e/vm_user_journey_test.go +++ b/test/e2e/vm_user_journey_test.go @@ -171,7 +171,7 @@ func TestVM_Journey_FullSetupConfiguresEverything(t *testing.T) { bin := vmCopyDevBinary(t, vm) output, err := vmRunDevBinaryWithGit(t, vm, bin, - "install --preset test-e2e-fixture --silent --shell install --dotfiles clone --macos configure") + "install --preset minimal --silent --shell install --dotfiles clone --macos configure") t.Logf("full setup:\n%s", output) require.NoError(t, err)