Close the SDD cycle for the data-loss shielding change: verified pass with warnings (0 critical, 10/10 scenarios with named tests, W1 fixed post-verify), three stacked work units shipped plus the path-resolution fix. The persistence-resilience capability spec is promoted to openspec/specs/.
6.0 KiB
Spec: Persistence Corruption Guard
New Capability: persistence-resilience
Purpose
Cross-cutting invariants so a corrupt or schema-incompatible read of persisted user data (alarms, custom stations, EQ presets/device names) never silently destroys prior valid data: per-entry tolerant parsing, never-fabricate identity, and a degraded-read guard that suppresses automatic writes until a good read or an explicit user mutation restores write authority.
Requirements
Requirement: Per-entry tolerant parsing preserves valid siblings
When a persisted collection decodes at the top level but one entry is malformed, the system MUST skip and log only that entry (index/key/id + reason), retaining every other valid entry. A skipped entry MUST NOT cause any sibling to be lost, regardless of whether a normalizing rewrite of the survivors later occurs — that mechanism is a design-time decision; this requirement constrains only the observable outcome.
Scenario: alarms — one corrupt alarm among N — Dart-testable via SharedPreferences mock seeding
- GIVEN
alarmas_musicales_v1has 3 valid alarms and 1 malformed entry - WHEN the config loads
- THEN
alarmasMUST contain exactly the 3 valid entries; corrupt one skipped + logged
Scenario: custom stations — one corrupt station among N — Dart-testable via injected temp-file fixture
- GIVEN the custom-stations file has 2 valid stations and 1 entry missing required fields
- WHEN custom stations load
- THEN
_emisorasCustomMUST contain exactly the 2 valid stations; corrupt one skipped + logged
Scenario: EQ maps — one corrupt value among N — Dart-testable via SharedPreferences mock seeding
- GIVEN a persisted EQ map (per-station/per-device/matrix presets, device names) has 3 valid entries and 1 entry whose value fails to parse
- WHEN that map loads
- THEN it MUST contain exactly the 3 valid entries under their original keys; corrupt one skipped + logged
Requirement: Entry identity is never fabricated
An entry whose identity field (alarm id, station uuid) is missing, empty, or the wrong type MUST be treated as corrupt and skipped per the requirement above. The system MUST NOT synthesize a substitute identity. (EQ entries are keyed by their persisted map key, always present by construction — no scenario needed here.)
Scenario: alarms — entry with no id is skipped, never assigned one — Dart-testable via SharedPreferences mock seeding
- GIVEN one persisted alarm entry has no
idfield - WHEN the config loads
- THEN that entry MUST be absent, and no alarm with a freshly generated id MUST appear in its place
Scenario: custom stations — entry with no uuid is skipped, never assigned one — Dart-testable via injected temp-file fixture
- GIVEN one persisted station entry has no
uuidfield - WHEN custom stations load
- THEN that entry MUST be absent, and no station with a freshly generated uuid MUST appear in its place
Requirement: Degraded top-level read suppresses automatic writes until authority is restored
When a persisted payload's top-level decode fails entirely, in-memory state MAY be empty for the session, but the system MUST NOT let any non-explicit write reach disk while degraded. Suppression MUST lift on the next successful read or an explicit user mutation, whichever comes first (user intent wins).
Scenario: alarms — recalcularTodas writes nothing after a degraded read; explicit save restores authority — Dart-testable via SharedPreferences mock seeding
- GIVEN
alarmas_musicales_v1is unparseable - WHEN the config loads and
recalcularTodas()runs - THEN no write occurs; the original string stays on disk
- AND WHEN the user calls
guardarAlarmaafterward - THEN the write reaches disk and later automatic writes resume
Scenario: custom stations — no automatic write after a degraded file; explicit add/remove restores authority — Dart-testable via injected temp-file fixture
- GIVEN the custom-stations file is unparseable
- WHEN custom stations load
- THEN no write to the file occurs
- AND WHEN the user calls
agregarEmisoraCustomoreliminarEmisoraCustom - THEN that write reaches the file and normal writes resume
Scenario: EQ — no automatic write after a degraded key; explicit tweak restores authority — Dart-testable via SharedPreferences mock seeding
- GIVEN one of the 4 EQ persistence keys is unparseable
- WHEN the EQ config loads
- THEN no
guardarXxxwrite occurs - AND WHEN the user performs an explicit EQ tweak (e.g.
guardarPresetDispositivo) - THEN that write reaches disk and normal writes resume for that key
Requirement: Healthy reads are behaviorally unchanged
For a payload that decodes fully and validly at container and entry level, this capability MUST NOT alter resulting state, timing, or persisted bytes versus pre-change behavior.
Scenario: existing suites lock byte-for-byte parity — Dart-testable (regression)
- GIVEN the existing green suites (
test/servicios/servicio_alarmas_*,test/estado/estado_radio_*,test/servicios/servicio_ecualizador_test.dart) seeded with well-formed data - WHEN they run unmodified against the changed code
- THEN every assertion MUST still pass with no behavioral difference
Requirement: Diagnostics are developer-facing only
Every skipped entry and every degraded read MUST log with context (subsystem, key/id/index, reason) via the existing [PluriWave]-prefixed convention. This capability MUST NOT introduce any user-visible string or l10n key.
Scenario: skip and degraded-read events are logged, no l10n added — Dart-testable (log/hook assertion)
- GIVEN a corrupt entry is skipped or a top-level decode fails
- WHEN that event occurs
- THEN a
[PluriWave]-prefixed log line MUST identify the subsystem and reason, and no string MUST be added tolib/l10n/app_en.arbor any locale file
Localization
Not applicable — no user-visible copy is introduced by this capability; all diagnostics are developer-facing log lines only.