wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
//! Convergence is where work gets destroyed if the rules are wrong. Every one
//! of these asserts about a user's data, not about a status word.

use super::*;

fn file(hash: &str) -> Entry {
    Entry {
        kind: EntryKind::File,
        hash: hash.to_string(),
        size: hash.len() as u64,
        executable: false,
    }
}

fn set(entries: &[(&str, Entry)]) -> BTreeMap<String, Entry> {
    entries
        .iter()
        .map(|(path, entry)| ((*path).to_string(), entry.clone()))
        .collect()
}

fn action_for<'a>(convergence: &'a Convergence, path: &str) -> Option<&'a Action> {
    convergence.actions.iter().find(|a| a.path() == path)
}

/// TEST9931: a change on one side moves; a change on neither does not.
///
/// The ordinary case, and the one that must never ask a question: if the peer
/// is still holding exactly what both sides last agreed on, taking this side's
/// edit cannot lose anything.
#[test]
fn test9931_a_one_sided_change_moves_without_asking() {
    let base = set(&[("a.rs", file("base")), ("b.rs", file("same"))]);
    let here = set(&[("a.rs", file("mine")), ("b.rs", file("same"))]);
    let there = set(&[("a.rs", file("base")), ("b.rs", file("same"))]);

    let plan = plan(&here, &there, &base);

    assert_eq!(
        action_for(&plan, "a.rs"),
        Some(&Action::Send {
            path: "a.rs".into(),
            entry: file("mine")
        })
    );
    // Untouched on both sides: no action at all, not an "agree".
    assert!(action_for(&plan, "b.rs").is_none());
    assert_eq!(plan.pending(), 1);
    assert!(!plan.is_converged());

    // The mirror image, from the other machine's point of view.
    let mirrored = super::plan(&there, &here, &base);
    assert_eq!(
        action_for(&mirrored, "a.rs"),
        Some(&Action::Receive {
            path: "a.rs".into(),
            entry: file("mine")
        })
    );
}

/// TEST9932: both sides changed the same base differently — nobody wins.
///
/// This is the case a last-writer-wins design silently destroys. There is no
/// timestamp anywhere in the decision, so a machine with a fast clock cannot
/// overwrite one with a slow clock.
#[test]
fn test9932_independent_changes_conflict_rather_than_race() {
    let base = set(&[("src/router.rs", file("base"))]);
    let here = set(&[("src/router.rs", file("desktop"))]);
    let there = set(&[("src/router.rs", file("laptop"))]);

    let plan = plan(&here, &there, &base);

    assert_eq!(
        action_for(&plan, "src/router.rs"),
        Some(&Action::Conflict {
            path: "src/router.rs".into(),
            here: Some(file("desktop")),
            there: Some(file("laptop")),
            base: Some(file("base")),
        })
    );
    // Nothing moves for a conflicted path.
    assert_eq!(plan.pending(), 0);
    assert_eq!(plan.conflicts().count(), 1);
    assert!(!plan.is_converged());
}

/// TEST9933: the same edit made on both machines is convergence, not conflict.
///
/// People do this constantly — apply the same one-line fix in two places, or
/// run the same formatter. Reporting it as a conflict would teach the operator
/// that conflicts are noise, which is exactly when a real one gets clicked
/// through.
#[test]
fn test9933_identical_edits_on_both_sides_converge() {
    let base = set(&[("x", file("old"))]);
    let both = set(&[("x", file("new"))]);

    let plan = plan(&both, &both, &base);

    assert_eq!(
        action_for(&plan, "x"),
        Some(&Action::Agree {
            path: "x".into(),
            entry: Some(file("new"))
        })
    );
    assert_eq!(plan.pending(), 0);
    assert_eq!(plan.conflicts().count(), 0);
    // Nothing has to move, so the tree is converged — but the BASELINE still
    // has to advance, or the next round decides both sides changed again.
    assert!(plan.is_converged());
    let advanced = advance(&base, &plan, &["x".to_string()].into_iter().collect());
    assert_eq!(advanced.get("x"), Some(&file("new")));
}

/// TEST9934: a deletion is a change like any other — and it never wins over an
/// edit.
///
/// Deleting a file the peer independently modified is the single most
/// destructive thing this feature could get wrong: the file is gone here, and
/// the version that had work in it is gone there too, with nothing to recover
/// from.
#[test]
fn test9934_a_deletion_never_destroys_an_independent_edit() {
    let base = set(&[("gone.rs", file("base")), ("kept.rs", file("base"))]);

    // Deleted here, untouched there: the deletion carries.
    let here = set(&[("kept.rs", file("base"))]);
    let there = set(&[("gone.rs", file("base")), ("kept.rs", file("base"))]);
    let plan = plan(&here, &there, &base);
    assert_eq!(
        action_for(&plan, "gone.rs"),
        Some(&Action::SendDeletion {
            path: "gone.rs".into()
        })
    );

    // Deleted here, MODIFIED there: conflict, and the modified side keeps its
    // file until a person says otherwise.
    let there_modified = set(&[("gone.rs", file("their work")), ("kept.rs", file("base"))]);
    let plan = super::plan(&here, &there_modified, &base);
    assert_eq!(
        action_for(&plan, "gone.rs"),
        Some(&Action::Conflict {
            path: "gone.rs".into(),
            here: None,
            there: Some(file("their work")),
            base: Some(file("base")),
        })
    );
    assert_eq!(plan.pending(), 0);

    // And the same in the other direction.
    let plan = super::plan(&there_modified, &here, &base);
    assert!(action_for(&plan, "gone.rs").expect("an action").is_conflict());
}

/// TEST9935: a file created on both sides with different content is a
/// conflict, and with the same content is not.
///
/// There is no baseline for a new path, so the "changed since base" question
/// has to treat absence as a state rather than as missing information.
#[test]
fn test9935_creations_without_a_baseline_are_still_compared() {
    let base = Baseline::new();

    let plan = plan(&set(&[("new", file("mine"))]), &set(&[("new", file("theirs"))]), &base);
    assert_eq!(
        action_for(&plan, "new"),
        Some(&Action::Conflict {
            path: "new".into(),
            here: Some(file("mine")),
            there: Some(file("theirs")),
            base: None,
        })
    );

    let same = set(&[("new", file("identical"))]);
    let plan = super::plan(&same, &same, &base);
    assert_eq!(
        action_for(&plan, "new"),
        Some(&Action::Agree {
            path: "new".into(),
            entry: Some(file("identical"))
        })
    );

    // Created on one side only: an ordinary send.
    let plan = super::plan(&set(&[("new", file("mine"))]), &BTreeMap::new(), &base);
    assert!(matches!(action_for(&plan, "new"), Some(Action::Send { .. })));
}

/// TEST9936: kind and mode are part of identity.
///
/// A file that became a symlink is not an edit, and losing an executable bit
/// breaks whatever runs it. Comparing content alone would call both of these
/// "unchanged".
#[test]
fn test9936_kind_and_executable_bit_are_part_of_identity() {
    let plain = file("same-bytes");
    let executable = Entry {
        executable: true,
        ..plain.clone()
    };
    let link = Entry {
        kind: EntryKind::Symlink,
        ..plain.clone()
    };

    assert!(!plain.same_as(&executable));
    assert!(!plain.same_as(&link));

    let base = set(&[("script", plain.clone())]);
    let here = set(&[("script", executable.clone())]);
    let plan = plan(&here, &set(&[("script", plain)]), &base);
    assert_eq!(
        action_for(&plan, "script"),
        Some(&Action::Send {
            path: "script".into(),
            entry: executable
        })
    );
}

/// TEST9937: the baseline advances only over what actually landed.
///
/// A baseline that moved for a path whose transfer failed would make the next
/// round believe both sides agreed — and the change would be silently dropped
/// on the floor instead of retried. A conflict must not move it either, or the
/// conflict evaporates on the next scan with one side's work lost.
#[test]
fn test9937_the_baseline_advances_only_over_what_landed() {
    let base = set(&[
        ("landed", file("base")),
        ("failed", file("base")),
        ("fought", file("base")),
    ]);
    let here = set(&[
        ("landed", file("mine")),
        ("failed", file("mine")),
        ("fought", file("mine")),
    ]);
    let there = set(&[
        ("landed", file("base")),
        ("failed", file("base")),
        ("fought", file("theirs")),
    ]);

    let plan = plan(&here, &there, &base);
    // Only the one that applied is acknowledged.
    let applied = ["landed".to_string()].into_iter().collect();
    let next = advance(&base, &plan, &applied);

    assert_eq!(next.get("landed"), Some(&file("mine")));
    assert_eq!(
        next.get("failed"),
        Some(&file("base")),
        "a transfer that did not land must leave the baseline where it was, so it is retried"
    );
    assert_eq!(
        next.get("fought"),
        Some(&file("base")),
        "a conflict must leave the baseline where it was, so it survives a restart"
    );

    // A deletion that landed removes the path from the baseline entirely,
    // rather than leaving a ghost that makes the next round see a creation.
    let deleted_here = set(&[("failed", file("base")), ("fought", file("mine"))]);
    let plan = super::plan(&deleted_here, &there, &base);
    let next = advance(&base, &plan, &["landed".to_string()].into_iter().collect());
    assert!(!next.contains_key("landed"));
}

/// TEST9995: agreement moves the baseline on both machines, with nothing sent.
///
/// The bug this exists for: a path both machines had already dropped stayed in
/// one machine's baseline forever. `Agree` is never in `applied` — nothing
/// travels for it — so gating the baseline on `applied` left a phantom entry
/// on whichever side had it. Every request carries a baseline digest, so the
/// two machines then refused each other permanently: both read as "checking",
/// nothing moved again, and no edit made afterwards could ever cross.
#[test]
fn test9995_agreement_advances_both_baselines_with_nothing_sent() {
    // A path the baseline remembers and NEITHER machine has any more.
    let base = set(&[("kept", file("same")), ("phantom", file("gone"))]);
    let here = set(&[("kept", file("same"))]);
    let there = set(&[("kept", file("same"))]);

    let plan = plan(&here, &there, &base);
    assert!(plan.is_converged(), "nothing has to move: {plan:?}");
    assert_eq!(plan.pending(), 0);

    // Nothing was transferred — there was nothing to transfer.
    let next = advance(&base, &plan, &BTreeSet::new());
    assert!(
        !next.contains_key("phantom"),
        "a path neither machine has must leave the baseline: {next:?}"
    );
    assert_eq!(next.get("kept"), Some(&file("same")));

    // And the two machines compute the SAME baseline from the same two scans,
    // which is what keeps their digests equal and their transfers accepted.
    let mirrored = advance(&base, &super::plan(&there, &here, &base), &BTreeSet::new());
    assert_eq!(next, mirrored);

    // The same holds for a path both machines edited to the same content: it
    // is agreement, not a transfer, and both sides record it.
    let base = set(&[("x", file("old"))]);
    let both = set(&[("x", file("new"))]);
    let plan = super::plan(&both, &both, &base);
    let next = advance(&base, &plan, &BTreeSet::new());
    assert_eq!(
        next.get("x"),
        Some(&file("new")),
        "identical edits are agreed, and an agreement nobody records is decided again forever"
    );
}

/// TEST9938: convergence is symmetric.
///
/// Whatever this side decides to send, the other side must decide to receive —
/// for every shape of change. A rule that is not symmetric is one where the
/// two machines disagree about who is authoritative, and the file ends up
/// wherever the last packet came from.
#[test]
fn test9938_every_decision_is_symmetric() {
    let base = set(&[
        ("edited-here", file("base")),
        ("edited-there", file("base")),
        ("deleted-here", file("base")),
        ("fought", file("base")),
    ]);
    let here = set(&[
        ("edited-here", file("mine")),
        ("edited-there", file("base")),
        ("fought", file("mine")),
        ("new-here", file("mine")),
    ]);
    let there = set(&[
        ("edited-here", file("base")),
        ("edited-there", file("theirs")),
        ("deleted-here", file("base")),
        ("fought", file("theirs")),
    ]);

    let mine = plan(&here, &there, &base);
    let theirs = plan(&there, &here, &base);

    for action in &mine.actions {
        let counterpart = action_for(&theirs, action.path()).expect("both sides see every path");
        let matched = match (action, counterpart) {
            (Action::Send { .. }, Action::Receive { .. }) => true,
            (Action::Receive { .. }, Action::Send { .. }) => true,
            (Action::SendDeletion { .. }, Action::ReceiveDeletion { .. }) => true,
            (Action::ReceiveDeletion { .. }, Action::SendDeletion { .. }) => true,
            (Action::Conflict { .. }, Action::Conflict { .. }) => true,
            (Action::Agree { .. }, Action::Agree { .. }) => true,
            _ => false,
        };
        assert!(
            matched,
            "{} is {action:?} here and {counterpart:?} there — the two sides disagree about who is authoritative",
            action.path()
        );
    }
    assert_eq!(mine.actions.len(), theirs.actions.len());
}

/// TEST11269: `auto` applies to what ARRIVES, and never to what leaves.
///
/// The whole point of pairing two machines with different modes. `auto` is a
/// statement a machine makes about its own working tree — "what arrives here
/// is applied without asking me". It says nothing about the peer's tree:
/// whether this machine's changes are applied there is the peer's decision,
/// made by the peer's own pass with the peer's own mode.
///
/// It used to gate both directions from one side, so a machine set to `auto`
/// pushed undecided changes onto a peer that had deliberately stayed manual —
/// the one thing manual exists to prevent. Two machines can now differ, and
/// changes travel automatically in one direction only.
#[test]
fn test11269_auto_takes_what_arrives_and_never_pushes_what_does_not() {
    use crate::peer::model::Continuity as C;

    let incoming = Action::Receive {
        path: "a.txt".into(),
        entry: file("theirs"),
    };
    let incoming_deletion = Action::ReceiveDeletion { path: "b.txt".into() };
    let outgoing = Action::Send {
        path: "c.txt".into(),
        entry: file("mine"),
    };
    let outgoing_deletion = Action::SendDeletion { path: "d.txt".into() };

    // Auto, nobody decided: what arrives is taken, what would leave is not.
    for action in [&incoming, &incoming_deletion] {
        assert!(
            crosses(action, C::Auto, false),
            "auto must take {}: it is what auto means",
            action.kind()
        );
    }
    for action in [&outgoing, &outgoing_deletion] {
        assert!(
            !crosses(action, C::Auto, false),
            "auto must not push {} — whether the peer takes it is the peer's decision",
            action.kind()
        );
    }

    // Manual, nobody decided: nothing crosses in either direction.
    for action in [&incoming, &incoming_deletion, &outgoing, &outgoing_deletion] {
        assert!(
            !crosses(action, C::Manual, false),
            "manual must not move {} unasked",
            action.kind()
        );
    }

    // A DECISION crosses either way, in both modes: `take` and `keep` are a
    // person on this machine saying which side wins.
    for mode in [C::Manual, C::Auto] {
        for action in [&incoming, &incoming_deletion, &outgoing, &outgoing_deletion] {
            assert!(
                crosses(action, mode, true),
                "{} must carry a decided {}",
                mode.as_str(),
                action.kind()
            );
        }
    }

    // Paused and off carry nothing, decided or not — a decision taken before a
    // pause is not consent to act during it.
    for mode in [C::Off, C::Paused] {
        for decided in [false, true] {
            for action in [&incoming, &outgoing] {
                assert!(
                    !crosses(action, mode, decided),
                    "{} must move nothing (decided={decided})",
                    mode.as_str()
                );
            }
        }
    }

    // Agreement is not a move and is not gated: two machines holding the same
    // bytes have agreed whatever the mode, and the baseline may say so.
    let agreed = Action::Agree {
        path: "e.txt".into(),
        entry: Some(file("same")),
    };
    for mode in [C::Manual, C::Auto] {
        assert!(crosses(&agreed, mode, false), "{} must record agreement", mode.as_str());
    }
}