feat(diff,engine): UserEnvSwitch.config_hash + state.toml [[managed.user_env]]#50
Merged
Merged
Conversation
…ser_env]] Plan §6.3 + §7.4 W2 — closes the M3 W2 user-env loop. Each UserEnvSwitch now carries the plan-time config_hash; the engine records (user, generation, config_hash) into state.toml after each successful switch, so the next pearlite plan sees no drift on a clean re-apply. Action::UserEnvSwitch gains config_hash: String — non-empty in the normal first-apply / drift path, empty sentinel in the defensive "declared HM enabled but config_path missing on disk" arm. The sentinel propagates verbatim into state.toml; the next plan recomputes and re-applies, which is correct behaviour for that edge case. UserToSwitch (the diff classifier's intermediate type) gains the same field. classify_user_env populates it from declared_user_env_hash[user] (or "" when missing). compose's user_env_actions threads it into the Action. Engine apply_plan grew a Vec<UserEnvRecord> accumulator that exec_action's UserEnvSwitch arm pushes to after a successful switch; the existing phase-9 commit calls a new upsert_user_env_records helper that replaces an existing record by user-name (rather than appending duplicates). The order is "last write wins" — a single apply emitting two switches for the same user (theoretically impossible since within_phase_key dedupes by login name, but the data structure allows it) keeps the later record. Two new engine tests: - user_env_record_upserts_on_re_apply — first apply records (alice, gen=1, hash=v1); second apply with hash=v2 *replaces* the entry rather than appending. - The existing user_env_switch_dispatches_through_home_manager test was extended to assert state.toml's managed.user_env grew the expected pair of records (alice + bob with their respective config_hashes). Two new diff classifier tests: - user_to_switch_carries_declared_fields now checks config_hash == "" for the no-hash path. - user_to_switch_carries_declared_hash_when_available — verifies the hash from declared_user_env_hash propagates to the action. Existing 5 UserEnvSwitch literals across diff + engine tests updated to pass the new config_hash field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
UnbreakableMJ
added a commit
that referenced
this pull request
Apr 28, 2026
Plan §7.4 W2 — closes the M3 W2 VM-tier line items "vm-07 user-env apply, vm-08 user-env drift". vm-07 declares one HM-enabled user with an inline home.nix fixture, applies, and verifies: - apply exit 0, actions_executed >= 1 - state.toml grew a [[managed.user_env]] entry for $PEARLITE_VM_USER with a non-empty 64-hex config_hash vm-08 exercises the drift loop end-to-end: 1. Apply with home.nix v1 → state.toml records hash_v1. 2. Re-plan (no edits) → no user_env_switch action emitted (idempotency). 3. Mutate home.nix to v2 → re-plan emits a user_env_switch action. 4. Apply v2 → state.toml's config_hash flips to hash_v2 AND managed.user_env still contains exactly one row for the user (upsert per PR #50, not append). Both scripts: - POSIX sh; shellcheck-clean. - Mutating, gated behind PEARLITE_VM_TEST=1 (the runner enforces). - Honour $PEARLITE_VM_USER (default `pearlite-vm`) for the test account; fail fast with a clear error if that user doesn't exist rather than calling useradd from inside the script (cleaner: the VM image owns user provisioning). - Use grep over the toml/json output rather than jaq so they run on minimal CachyOS images, matching the convention from vm-02..06. vm-09 (Nix bootstrap via DeterminateNixInstaller) defers — the LiveNixInstaller adapter exists but no caller wires it yet; the bootstrap-via-curl-pipe behaviour belongs in M3 follow-up. README updated with the new rows. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plan §6.3 + §7.4 W2 — closes the M3 W2 user-env loop. Each
UserEnvSwitchnow carries the plan-timeconfig_hash; the engine records(user, generation, config_hash)intostate.tomlafter each successful switch, so the nextpearlite plansees no drift on a clean re-apply.Changes
Action::UserEnvSwitchgainsconfig_hash: String. Non-empty in the normal first-apply / drift path, empty sentinel in the defensive "declared HM enabled but config_path missing on disk" arm. The sentinel propagates verbatim; the next plan recomputes and re-applies (correct behaviour for that edge case).UserToSwitch(diff classifier intermediate) gains the same field;classify_user_envpopulates fromdeclared_user_env_hash[user].compose::user_env_actionsthreads it into theAction.Engine::apply_plangrew aVec<UserEnvRecord>accumulator thatexec_action'sUserEnvSwitcharm pushes to after a successful switch. The phase-9 commit calls a newupsert_user_env_recordshelper that replaces an existing record by user-name (rather than appending duplicates).upsert_user_env_recordsis "last write wins" by user.within_phase_keyalready dedupes by login name in the action ordering, so this is belt-and-braces.Test plan
user_env_record_upserts_on_re_apply— first apply records(alice, gen=1, hash=v1); second apply withhash=v2replaces the entry rather than appending.user_env_switch_dispatches_through_home_managerextended to assertstate.toml'smanaged.user_envgrew the expected pair of records (alice + bob with their respectiveconfig_hashes).user_to_switch_carries_declared_fieldschecksconfig_hash == ""for the no-hash path.user_to_switch_carries_declared_hash_when_availableverifies the hash fromdeclared_user_env_hashpropagates to the action.UserEnvSwitchliterals across diff + engine tests updated to pass the newconfig_hashfield.cargo fmt --all -- --check,cargo clippy --workspace --all-targets --all-features -- -D warnings, full workspacecargo test, SPDX,pearlite-audit check .all green.🤖 Generated with Claude Code