1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! P9 — undo and redo as a property.
//!
//! The claim is algebraic and users state it themselves: whatever I just did,
//! `u` puts it back. Written out over a whole session:
//!
//! > For a settled project and any sequence of TUI actions — undoing everything
//! > restores the project byte for byte, and redoing everything restores the
//! > result byte for byte.
//!
//! Both halves earn their place. `undone == start` is the claim users rely on.
//! `redone == end` is the one that catches an operation that records too little
//! to replay — which is exactly the shape of the `TrackAdd` display-name loss,
//! where redo re-created a track under a derived name instead of the one the
//! user typed.
//!
//! ## Why this drives `App`, not `UndoStack`
//!
//! `UndoStack::undo` is only half the undo path. Eight of the `Operation`
//! variants — every `Track*` one — hit a match arm in `apply_inverse` that
//! returns `None` with the comment "handled by the caller (needs config +
//! filesystem access)". The caller is `apply_nav_side_effects` in
//! `src/tui/input/common.rs`: the code that rewrites `project.toml`, deletes and
//! re-creates track files, and moves files in and out of `archive/_tracks/`.
//!
//! Both undo defects this project has actually shipped lived there, not in
//! `undo.rs` — F3 (delete a track, undo, get an empty shell back) and the
//! `TrackAdd` display-name loss. A property over
//! `UndoStack::undo(&mut tracks, inbox)` would have passed both times. So the
//! unit under test is the App-level undo, driven the way a user drives it and
//! observed where the damage lands: on disk.
//!
//! ## Three things that follow from that
//!
//! **Actions are performed, not synthesised.** Steps are pressed through
//! `handle_key`, as in `parity.rs`. Pushing generated `Operation` values would
//! test a fiction — a `SectionMove { from_index: 7 }` that no code path
//! produces proves nothing about code paths that do. Performing the action means
//! the recorded operation is whatever the real handler recorded, which is where
//! the bugs are.
//!
//! **Steps are semantic, not keystrokes.** Random `KeyEvent`s spend their time
//! navigating and occasionally wander into edit mode to type garbage. A
//! `tui_steps::Step` is a (which task, which action) pair; the generator emits
//! indices and the runner resolves them against the live app, so a step always
//! names a task that exists at the moment it runs.
//!
//! **The comparison is the on-disk tree.** Model equality is meaningless here:
//! `source_text` means a clean task serialises verbatim while a touched one
//! serialises canonically, so `Task`-level equality would be trivially true or
//! trivially false depending on `dirty`. `tui_steps::frame_tree` is every file
//! under `frame/` minus `LOCAL_ONLY_FRAME_FILES` — fourth consumer of that
//! constant, after `fr init`, `fr check` and `parity.rs`.
//!
//! All three live in `tests/support/tui_steps.rs`, shared with P8
//! (`concurrency.rs`), which drives the same steps against a project a second
//! writer is also writing to. What stays here is the undo-specific part: press
//! `u` until the stack is empty, press `Z` until it is empty the other way, and
//! compare.
//!
//! The fixture must be **settled** (`serialize(parse(x)) == x`), or the first
//! `mark_dirty` canonicalises a task and every diff is noise;
//! [`the_fixture_is_settled`] pins that — for both suites, since both build the
//! same fixture.
//!
//! ## Byte-exact, and unqualified
//!
//! The comparison was expected to need an exemption list: byte-exact flags
//! churn that is not data loss, and `parity.rs` carries `known_divergence` for
//! exactly that. It turned out not to. Every divergence this suite found on the
//! way in was a defect worth fixing — including the two that looked cosmetic, a
//! blank line at end of file and a doubled separator in a drained section, both
//! of which turned out to be *accumulating* and invisible to every other check
//! because the result round-trips through the parser unchanged.
//!
//! So the comparison stands with nothing carved out of it. A future divergence
//! gets a fix or a stated exemption; it does not get quietly excluded from the
//! tree.
//!
//! ## What it cannot see
//!
//! Anything the forward path and the undo path agree on. A writer that formats
//! something badly but *consistently* produces that formatting in `end`, again
//! in `redone`, and undoes cleanly back to `start` — three passing comparisons
//! and a wrong file. The blank line welded under a bare section header is
//! exactly that shape, and it is pinned in `track_serializer.rs` instead.
//!
//! Which is the boundary of the whole suite, worth stating plainly: this asks
//! whether undo is the inverse of what was done, not whether what was done was
//! right.
use fs;
use *;
use ;
use ;
/// Press undo until the stack is empty. The cap is a guard against an operation
/// that puts itself back — `SyncMarker` does exactly that by design.
// ---------------------------------------------------------------------------
// The property
// ---------------------------------------------------------------------------
proptest!
// ---------------------------------------------------------------------------
// Fixed cases
// ---------------------------------------------------------------------------
/// The precondition the whole suite rests on: the fixture is a fixpoint of the
/// parse/serialize pair, **and stays one once everything in it is dirty**.
///
/// The second half is the one that matters here and it is not the usual
/// settledness check. A clean task or inbox item is emitted from its
/// `source_text` verbatim, so a file can round-trip perfectly and still be
/// spelled differently from the way the serializer would spell it. The moment a
/// step touches such a record, it comes back canonical — and P9 would report
/// that as undo failing to restore the project, when nothing was lost at all.
/// Marking everything dirty asks the question P9 actually needs answered: is
/// this file already in the form the writer produces?
///
/// It is stated once here rather than in both suites that build this fixture:
/// `concurrency.rs` shares it through `tests/support/tui_steps.rs` and rests on
/// the same precondition for the same reason.
/// Mark every task and subtask dirty, so the serializer emits its canonical
/// form rather than the source lines it was parsed from.