Skip to content

feat(state): migrate framework, MockFs, proptest — closes Plan §6.2#8

Merged
UnbreakableMJ merged 1 commit into
mainfrom
feat/state-migrate-mock-proptest
Apr 27, 2026
Merged

feat(state): migrate framework, MockFs, proptest — closes Plan §6.2#8
UnbreakableMJ merged 1 commit into
mainfrom
feat/state-migrate-mock-proptest

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Chunk M1-W1-F of M1 plan — closes Plan §6.2.

What lands

Migration framework (`migrate.rs`)

`pub fn migrate(State) -> Result<State, StateError>` bumps schema_version forward one step at a time. v0 → v1 is the only transformation at M1. `StateStore::read` calls `migrate` automatically; `schema_version` is now `#[serde(default)]` so pre-versioned alpha files (no `schema_version` key) deserialize as 0 and upgrade to 1.

`MockFs` (`mock.rs`)

Compiled in `cargo test` (no feature) and behind `feature = "test-mocks"` for downstream consumers (engine integration tests in M2+). `Arc<Mutex<BTreeMap<PathBuf, Vec>>>` backing; `fail_next_rename()` / `fail_next_fsync_dir()` for single-shot failure injection.

Property test (proptest)

`arbitrary_state_payload_round_trips_through_mockfs` — randomized host, tool_version, managed.pacman / cargo, adopted.pacman survive write→read losslessly. Plan §6.2's "arbitrary State round-trips" gate.

Plan §6.2 Done when status

  • Triple-fsync atomic write tested via MockFs failure injection.
  • [⚠️] `toml_edit` preserves `[reserved]` and any unknown fields verbatim. Partial: `[reserved]` round-trips losslessly via the typed `BTreeMap<String, toml::Value>` field. Byte-level preservation of unknown fields outside `[reserved]` would need a `toml_edit`-based writer; deferred — the `[reserved]` namespace covers the documented forward-compat contract.
  • `schema_version` migration framework exists; trivial v0→v1 stub passes a fixture.
  • No `std::fs::*` calls outside `io.rs`.
  • `pearlite-audit` reports no §13 violations.
  • [⏭️] `cargo bench` 1 MB read+parse < 50 ms. Deferred: `benches/` harness lands once the engine is up and a realistic 1 MB fixture exists.

Verification

  • `cargo test -p pearlite-state` — 18 passed; 0 failed
  • `cargo clippy --workspace --all-targets -- -D warnings`
  • `cargo fmt --all --check`
  • `scripts/ci/check-spdx.sh`
  • `pearlite-audit check .` — 1 check, 0 violations

🤖 Generated with Claude Code

Chunk M1-W1-F of M1 plan — closes Plan §6.2.

Migration framework (migrate.rs):
- pub fn migrate(State) -> Result<State, StateError> bumps the
  schema_version field forward through one transformation per
  on-disk version. v0 → v1 is the only step at M1; future bumps
  add another `migrate_vN_to_vM` and one extra branch.
- StateStore::read calls migrate so the read path automatically
  upgrades older files in memory before returning to the caller.
- schema_version field gains #[serde(default)] so pre-versioned
  alpha files (no schema_version key) deserialize as 0 and migrate
  to 1.
- 3 unit tests: v0_to_v1_adds_schema_version,
  current_version_is_a_no_op, future_version_rejected.

MockFs (mock.rs):
- Compiled in cargo test (no feature) and behind feature = "test-mocks"
  for downstream consumers (the engine's integration tests in M2+).
- Backed by Arc<Mutex<BTreeMap<PathBuf, Vec<u8>>>>.
- Failure injection via fail_next_rename() and fail_next_fsync_dir(),
  both self-clearing single-shots.
- 4 own tests: seed_then_read_round_trip, read_missing_yields_not_found,
  injected_rename_failure_leaves_old_content, fail_injection_is_single_shot.
- Module-level allow(clippy::expect_used) for the standard Mutex<T>
  unwrap idiom — MockFs is test-utility code.

StateStore tests using MockFs (store.rs):
- rename_failure_via_mockfs_leaves_old_state — atomic-write guarantee
  verified through the trait, not through a tempdir.
- fsync_dir_failure_via_mockfs_propagates — error path.

Property test (store.rs, proptest):
- arbitrary_state_payload_round_trips_through_mockfs — randomized host,
  tool_version, managed.pacman/cargo, adopted.pacman survive a
  write→read cycle losslessly. Plan §6.2 acceptance:
  "property: arbitrary State round-trips losslessly (proptest-driven)."

Cargo.toml:
- New [features] block with test-mocks = [].
- proptest = "1" added to [dev-dependencies].

Plan §6.2 "Done when" status:

- Triple-fsync atomic write tested via MockFs failure injection. ✓
- toml_edit preserves [reserved] and any unknown fields verbatim.
  PARTIAL: [reserved] is preserved (BTreeMap<String, toml::Value>).
  Byte-level preservation of unknown fields outside [reserved] would
  require a toml_edit-based writer; deferred — the [reserved]
  namespace covers the documented forward-compat contract.
- schema_version migration framework exists; trivial v0→v1 stub
  passes a fixture. ✓
- No std::fs::* calls outside io.rs. ✓
- pearlite-audit reports no §13 violations. ✓
- cargo bench shows full read+parse of a 1 MB state.toml under 50 ms.
  DEFERRED: a benches/ harness lands as a follow-up after the engine
  is up and we have a realistic 1 MB fixture.

Verification:
- cargo test -p pearlite-state  (18 passed; 0 failed)
- cargo clippy --workspace --all-targets -- -D warnings  (zero warnings)
- cargo fmt --all --check
- scripts/ci/check-spdx.sh
- pearlite-audit check .  (1 check, 0 violations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit 6ce3b46 into main Apr 27, 2026
3 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feat/state-migrate-mock-proptest branch April 27, 2026 16:29
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