Skip to content

feat(diff,engine): UserEnvSwitch.config_hash + state.toml [[managed.user_env]]#50

Merged
UnbreakableMJ merged 1 commit into
mainfrom
feat/engine-user-env-state-record
Apr 28, 2026
Merged

feat(diff,engine): UserEnvSwitch.config_hash + state.toml [[managed.user_env]]#50
UnbreakableMJ merged 1 commit into
mainfrom
feat/engine-user-env-state-record

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Summary

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.

Changes

  • 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; the next plan recomputes and re-applies (correct behaviour for that edge case).
  • UserToSwitch (diff classifier intermediate) gains the same field; classify_user_env populates from declared_user_env_hash[user]. compose::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 phase-9 commit calls a new upsert_user_env_records helper that replaces an existing record by user-name (rather than appending duplicates).

upsert_user_env_records is "last write wins" by user. within_phase_key already dedupes by login name in the action ordering, so this is belt-and-braces.

Test plan

  • 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.
    • user_env_switch_dispatches_through_home_manager 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 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.
  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, full workspace cargo test, SPDX, pearlite-audit check . all green.

🤖 Generated with Claude Code

…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>
@UnbreakableMJ
UnbreakableMJ merged commit 0ed8664 into main Apr 28, 2026
3 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feat/engine-user-env-state-record branch April 28, 2026 20:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant