shep 0.18.2

The shep binary: a process manager that keeps a flock of long-running processes alive on macOS, Linux and Windows, with logs, watch and cron restarts, and webhook alerts
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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
//! The dog pane's write: every filed edit applied to the section's own TOML
//! text, ready for `Request::SetDogConfig`.
//!
//! `toml_edit` rather than a re-render of the pane's parsed values, and that
//! is the whole reason this module exists: the request replaces the table
//! wholesale, so a re-render would delete every comment in it on the
//! operator's own keystroke. `dogs.toml` is the operator's file, and shep
//! never writes it any other way.
//!
//! A row key is dotted (`models.worker.model`) whenever the dog's schema
//! nests a table, and the pane keeps the key-to-path map [`flattened`] built
//! the rows from beside the section text. The path is what this writer walks,
//! never the key: a property literally named `a.b` is one key, and the
//! section spells it `"a.b" = ...`.
//!
//! A field inside an entry of an array or map of tables is addressed the
//! same way, through a path of [`PathStep`]s that adds the entry's index in
//! an array. The entry is found and never created: an index past the end of
//! the array and a map key the section lacks write nothing.

use serde_json::Value;

use super::super::edits::{EditKey, Edits};
#[cfg(doc)]
use super::super::field::flattened;
use super::super::field::{Entries, FieldKind};
use super::entries_write::set_entries;
use super::{ConfigPane, PaneEdit, PathStep, SectionWrite};

impl ConfigPane {
    /// The section with every field edit in `edits` applied at its own path,
    /// comments and key order intact, ready for `Request::SetDogConfig`.
    ///
    /// This is what a close actually sends: the request replaces the table,
    /// so a batch of edits to one dog is one write, not one per entry. Each
    /// is applied to the document the previous one produced, walked in
    /// [`Edits::iter`]'s own key order.
    ///
    /// An [`EditKey::Env`] entry has no home in a dog's section, a dog
    /// having no env store, and is skipped rather than becoming a key in it.
    ///
    /// A `null` value removes the leaf and nothing above it, which is how the
    /// pane's empty buffer unsets one, and is what puts the dog back on its
    /// own default. A `null` on a key the section does not carry changes
    /// nothing and is not a failure: the section already says the key is
    /// absent, and refusing such an edit would cost every other edit in the
    /// close its write.
    ///
    /// A filed key that names no row of this pane is refused, and so is a
    /// field of an entry the section does not hold: a refresh can remove the
    /// entry an edit was filed under, and writing the edit anywhere else
    /// would put a key in the operator's file that they never typed.
    ///
    /// [`SectionWrite::Nothing`] for a sheep pane, which holds no section, for
    /// a set carrying no field edit, and for a batch whose every edit the
    /// section already carries. [`SectionWrite::Refused`] for a section that
    /// does not parse, and for any one entry this writer cannot land: a value
    /// with no TOML type, or a path that lands on something other than the
    /// shape the row edits (a scalar where the schema says a table, a table
    /// where the row edits one key inside it). A partial section is worse than
    /// none, since the request would replace the table with it.
    #[must_use]
    pub fn edited_section_with(&self, edits: &Edits) -> SectionWrite {
        let Some(section) = self.section_text() else {
            return SectionWrite::Nothing;
        };
        let Ok(mut doc) = section.parse::<toml_edit::DocumentMut>() else {
            return SectionWrite::Refused;
        };
        let mut changed = false;
        for (key, edit) in edits.iter() {
            let EditKey::Field(name) = key else {
                continue;
            };
            let PaneEdit::Set { value, .. } = edit.edit() else {
                continue;
            };
            let value = value.as_value();
            if let Some((kind, path)) = self.entries_field(name) {
                match set_entries(&mut doc, kind, path, value) {
                    Landed::Changed => changed = true,
                    Landed::Already => {}
                    Landed::Refused => return SectionWrite::Refused,
                }
                continue;
            }
            let Some((path, existing)) = self.write_path(name) else {
                return SectionWrite::Refused;
            };
            let landed = match toml_cell(value) {
                Some(cell) => set_at(doc.as_table_mut(), &path, existing, cell, false),
                // `null` has no TOML cell: it is the removal, not a value.
                None if value.is_null() => remove_at(doc.as_table_mut(), &path, existing),
                None => Landed::Refused,
            };
            match landed {
                Landed::Changed => changed = true,
                Landed::Already => {}
                Landed::Refused => return SectionWrite::Refused,
            }
        }
        if changed {
            SectionWrite::Written(doc.to_string().into())
        } else {
            SectionWrite::Nothing
        }
    }
}

impl ConfigPane {
    /// The kind and section path of the array or map of tables `key` names,
    /// or [`None`] for any other key.
    pub(super) fn entries_field(&self, key: &str) -> Option<(Entries, &[String])> {
        let FieldKind::Entries(kind) = self.fields.by_key(key)?.kind else {
            return None;
        };
        Some((kind, self.section_paths.get(key)?))
    }

    /// Where the row `key` is in the section, and how many leading steps of
    /// that path the section must already hold. [`None`] for a key that
    /// names no row.
    fn write_path(&self, key: &str) -> Option<(Vec<PathStep>, usize)> {
        if let Some(leaf) = self.entries.leaf(key) {
            return Some((leaf.at.to_vec(), leaf.entry_len));
        }
        let path = self.section_paths.get(key)?;
        Some((path.iter().cloned().map(PathStep::Key).collect(), 0))
    }
}

/// What one edit left behind it in the document.
///
/// Three states because a batch is judged as a whole: one entry that cannot
/// land loses the whole close, while an entry the section already carries must
/// not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Landed {
    /// The document now says something it did not before.
    Changed,
    /// The document already said this, so it is unchanged: a `null` on a key
    /// the section does not carry.
    Already,
    /// The path lands on something other than the table or the key this row
    /// edits, so nothing was written anywhere in the document.
    Refused,
}

/// The TOML item a filed value becomes, or [`None`] for one this writer has
/// no arm for.
///
/// An array is a cell only when every element is a string or every element is
/// an `i64`, the two shapes the list sub-screen files; an empty one is both.
/// Any other array, and a map, is refused: writing one as JSON text would put
/// a string in `dogs.toml` where the dog reads a table or a list. A number
/// that is neither an `i64` nor a genuine float (a `u64` above `i64::MAX`) is
/// refused for the same reason: the float it would round to reads back as
/// something else.
pub(super) fn toml_cell(value: &Value) -> Option<toml_edit::Item> {
    match value {
        Value::Bool(flag) => Some(toml_edit::value(*flag)),
        Value::Number(number) => {
            if let Some(int) = number.as_i64() {
                Some(toml_edit::value(int))
            } else if number.is_f64() {
                number.as_f64().map(toml_edit::value)
            } else {
                None
            }
        }
        Value::String(text) => Some(toml_edit::value(text.as_str())),
        Value::Array(elements) => {
            toml_array(elements).map(|array| toml_edit::Item::Value(toml_edit::Value::Array(array)))
        }
        Value::Null | Value::Object(_) => None,
    }
}

/// `elements` as a TOML array, or [`None`] unless they are all strings or all
/// integers.
fn toml_array(elements: &[Value]) -> Option<toml_edit::Array> {
    if elements.iter().all(Value::is_string) {
        elements
            .iter()
            .map(|element| element.as_str().map(toml_edit::Value::from))
            .collect()
    } else {
        elements
            .iter()
            .map(|element| element.as_i64().map(toml_edit::Value::from))
            .collect()
    }
}

/// Sets `cell` at `path`, creating any table on the path the section does not
/// carry yet.
///
/// A parent that is there takes the set where it is: a `[models.worker]`
/// header, a `models.worker.model = ...` dotted key and a
/// `worker = { model = ... }` inline table are all [`toml_edit::TableLike`],
/// and each renders back in the form it came in. A parent the section lacks is
/// created implicit, so the write adds `[models.worker]` and no empty
/// `[models]` header above it, and inside an inline table it is created as an
/// inline table, which is the only kind that renders there.
///
/// The first `existing` steps are the exception: they name an entry of an
/// array or map of tables, which the operator's section either holds or does
/// not, and [`Landed::Refused`] is the answer when it does not.
///
/// [`Landed::Refused`] where the section already holds something else at a
/// step of the path: `models = "old"` there is the operator's value, and a
/// nested row is not licensed to trade it for a table. The same for a leaf
/// that is already a table, which no row of this pane edits.
///
/// `inline` says whether `table` is an inline table, which is not something
/// [`toml_edit::TableLike`] answers: a table created inside one has to be an
/// inline table too, or it renders as nothing.
pub(super) fn set_at(
    table: &mut dyn toml_edit::TableLike,
    path: &[PathStep],
    existing: usize,
    cell: toml_edit::Item,
    inline: bool,
) -> Landed {
    let Some((PathStep::Key(key), rest)) = path.split_first() else {
        return Landed::Refused;
    };
    if let Some((PathStep::Index(at), after)) = rest.split_first() {
        let Some((entry, entry_inline)) =
            table.get_mut(key).and_then(|array| element_at(array, *at))
        else {
            return Landed::Refused;
        };
        return set_at(entry, after, existing.saturating_sub(2), cell, entry_inline);
    }
    if existing > 0 && table.get(key).is_none_or(toml_edit::Item::is_none) {
        return Landed::Refused;
    }
    // `entry` rather than `insert`: `insert` re-formats the key it replaces,
    // and that is where a comment the operator wrote above the key goes.
    let entry = table.entry(key).or_insert(toml_edit::Item::None);
    if rest.is_empty() {
        if entry.is_table_like() {
            return Landed::Refused;
        }
        *entry = cell;
        return Landed::Changed;
    }
    let nested_inline = if entry.is_none() {
        *entry = if inline {
            inline_child()
        } else {
            implicit_table()
        };
        inline
    } else if entry.is_inline_table() {
        true
    } else if !entry.is_table_like() {
        return Landed::Refused;
    } else {
        false
    };
    let Some(nested) = entry.as_table_like_mut() else {
        return Landed::Refused;
    };
    set_at(
        nested,
        rest,
        existing.saturating_sub(1),
        cell,
        nested_inline,
    )
}

/// Removes the leaf `path` names, and nothing above it.
///
/// [`Landed::Already`] when the leaf, or a table on the way to it, is not in
/// the section: the key is already unset, which is what the edit wanted. An
/// entry that is not there is the exception, as it is for [`set_at`]:
/// [`Landed::Refused`], since the edit was aimed at something that is gone.
/// [`Landed::Refused`] too when a step on the path is there and holds no keys,
/// so the removal was aimed at something that is not a table.
pub(super) fn remove_at(
    table: &mut dyn toml_edit::TableLike,
    path: &[PathStep],
    existing: usize,
) -> Landed {
    let Some((PathStep::Key(key), rest)) = path.split_first() else {
        return Landed::Refused;
    };
    if let Some((PathStep::Index(at), after)) = rest.split_first() {
        let Some((entry, _)) = table.get_mut(key).and_then(|array| element_at(array, *at)) else {
            return Landed::Refused;
        };
        return remove_at(entry, after, existing.saturating_sub(2));
    }
    if rest.is_empty() {
        return if table.remove(key).is_some() {
            Landed::Changed
        } else {
            Landed::Already
        };
    }
    let Some(item) = table.get_mut(key) else {
        return if existing > 0 {
            Landed::Refused
        } else {
            Landed::Already
        };
    };
    if matches!(item, toml_edit::Item::None) {
        return Landed::Already;
    }
    let Some(nested) = item.as_table_like_mut() else {
        return Landed::Refused;
    };
    remove_at(nested, rest, existing.saturating_sub(1))
}

/// Entry `at` of the array of tables `item` holds, and whether it is an inline
/// table. [`None`] for an index past the end, and for an item that is not an
/// array of tables.
fn element_at(
    item: &mut toml_edit::Item,
    at: usize,
) -> Option<(&mut dyn toml_edit::TableLike, bool)> {
    match item {
        toml_edit::Item::ArrayOfTables(tables) => tables
            .get_mut(at)
            .map(|table| (table as &mut dyn toml_edit::TableLike, false)),
        toml_edit::Item::Value(toml_edit::Value::Array(array)) => array
            .get_mut(at)?
            .as_inline_table_mut()
            .map(|table| (table as &mut dyn toml_edit::TableLike, true)),
        _ => None,
    }
}

/// A table `toml_edit` will not print a header for while it has no values of
/// its own, which is what keeps a created parent from showing as `[models]`.
pub(super) fn implicit_table() -> toml_edit::Item {
    let mut table = toml_edit::Table::new();
    table.set_implicit(true);
    toml_edit::Item::Table(table)
}

/// The kind of table to create inside an inline table. An inline table renders
/// its values and its inline tables and nothing else, so a regular
/// [`toml_edit::Item::Table`] inserted there reports a set and renders as
/// though nothing was written.
pub(super) fn inline_child() -> toml_edit::Item {
    toml_edit::Item::Value(toml_edit::Value::InlineTable(toml_edit::InlineTable::new()))
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::super::fixtures::{bark_pane, field, money_pane, web};
    use super::*;

    /// A decimal filed for a dog's section lands as a float, in the key's own
    /// place, with the comment the operator wrote above it. `toml_cell` already
    /// had the float arm; this pins that the float the pane now files reaches it
    /// rather than arriving as a string or being truncated to an integer.
    #[test]
    fn a_float_replaced_in_a_dogs_section_keeps_the_comment_above_it() {
        let mut pane = money_pane("# money cap\nper_item_usd = 10\nname = \"kelpie\"\n");
        pane.move_to_key("per_item_usd");
        pane.begin_typing();
        for _ in 0..10 {
            pane.type_backspace();
        }
        for c in "12.5".chars() {
            pane.type_char(c);
        }
        assert_eq!(pane.apply_typing(), None, "12.5 is above the floor");
        let SectionWrite::Written(section) = pane.edited_section_with(pane.edits()) else {
            panic!("the fixture section parses, and the replacement lands in it");
        };
        assert_eq!(
            section.as_str(),
            "# money cap\nper_item_usd = 12.5\nname = \"kelpie\"\n"
        );
    }

    /// A whole number typed into a decimal field is written back with its
    /// decimal point, which is what lets the dog read an `f64` where its own
    /// schema says `f64`.
    #[test]
    fn a_whole_number_filed_for_a_number_field_writes_a_float() {
        let mut pane = money_pane("per_item_usd = 12.5\n");
        pane.move_to_key("per_item_usd");
        pane.begin_typing();
        for _ in 0..4 {
            pane.type_backspace();
        }
        for c in "10".chars() {
            pane.type_char(c);
        }
        assert_eq!(pane.apply_typing(), None, "10 is a number");
        let SectionWrite::Written(section) = pane.edited_section_with(pane.edits()) else {
            panic!("the fixture section parses, and the replacement lands in it");
        };
        assert_eq!(section.as_str(), "per_item_usd = 10.0\n");
    }

    /// A dog whose schema nests one table inside another through a `$ref`
    /// chain, holds a secret beside a plain field in the inner one, and has
    /// a property literally named with a dot.
    fn nested_schema() -> Value {
        json!({
            "properties": {
                "name": { "type": "string" },
                "models": { "$ref": "#/$defs/Models" },
                "a.b": { "type": "string" },
            },
            "$defs": {
                "Models": {
                    "type": "object",
                    "properties": { "worker": { "$ref": "#/$defs/Worker" } },
                },
                "Worker": {
                    "type": "object",
                    "properties": {
                        "model": { "type": "string" },
                        "token": { "type": "string", "x-shep-secret": true },
                    },
                },
            },
        })
    }

    fn nested_pane(section: &str) -> ConfigPane {
        ConfigPane::dog("jobs".into(), None, nested_schema(), section.to_owned())
    }

    /// [`nested_schema`] with a list on the top level and one a table down.
    fn list_pane(section: &str) -> ConfigPane {
        let mut schema = nested_schema();
        schema["properties"]["hosts"] = json!({ "type": "array", "items": { "type": "string" } });
        schema["properties"]["codes"] = json!({ "type": "array", "items": { "type": "integer" } });
        schema["$defs"]["Worker"]["properties"]["models"] =
            json!({ "type": "array", "items": { "type": "string" } });
        ConfigPane::dog("jobs".into(), None, schema, section.to_owned())
    }

    /// The same data in the three forms `dogs.toml` allows for a nested
    /// table, each with a comment above the table and one inside it.
    const HEADER: &str = concat!(
        "# which model to run\n",
        "name = \"small\"\n",
        "\n",
        "[models.worker]\n",
        "# the credential\n",
        "token = \"sk-old\"\n",
        "model = \"gpt\"\n",
    );
    const DOTTED: &str = concat!(
        "# which model to run\n",
        "name = \"small\"\n",
        "models.worker.token = \"sk-old\"\n",
        "models.worker.model = \"gpt\"\n",
    );
    const INLINE: &str = concat!(
        "# which model to run\n",
        "name = \"small\"\n",
        "models = { worker = { token = \"sk-old\", model = \"gpt\" } }\n",
    );

    /// The header form with the credential left out, which is how an unset
    /// optional field reads in `dogs.toml`.
    const NO_TOKEN: &str = concat!(
        "# which model to run\n",
        "name = \"small\"\n",
        "\n",
        "[models.worker]\n",
        "model = \"gpt\"\n",
    );

    fn edits_of(pairs: &[(&str, Value)]) -> Edits {
        let mut edits = Edits::default();
        for (key, value) in pairs {
            edits.set(field(key, value.clone()), None);
        }
        edits
    }

    /// The section a write produced, panicking on either other answer so a
    /// test that wants text fails naming what it got instead.
    fn want_written(write: SectionWrite) -> String {
        match write {
            SectionWrite::Written(toml) => toml.as_str().to_owned(),
            other => panic!("wanted a section to send, got {other:?}"),
        }
    }

    /// Every form takes the set where it is, and neither comment survives
    /// anywhere but where the operator wrote it.
    #[test]
    fn a_nested_set_lands_in_the_form_the_section_already_uses() {
        for (section, want) in [
            (
                HEADER,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    "\n",
                    "[models.worker]\n",
                    "# the credential\n",
                    "token = \"sk-old\"\n",
                    "model = \"large\"\n",
                ),
            ),
            (
                DOTTED,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    "models.worker.token = \"sk-old\"\n",
                    "models.worker.model = \"large\"\n",
                ),
            ),
            (
                INLINE,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    "models = { worker = { token = \"sk-old\", model = \"large\" } }\n",
                ),
            ),
        ] {
            let out = want_written(
                nested_pane(section)
                    .edited_section_with(&edits_of(&[("models.worker.model", json!("large"))])),
            );
            assert_eq!(out, want, "{section}");
        }
    }

    /// A set into a table the section does not carry writes the deepest
    /// header it needs and nothing above it.
    #[test]
    fn a_set_that_creates_a_table_writes_no_empty_parent_header() {
        let out = want_written(
            nested_pane("name = \"small\"\n")
                .edited_section_with(&edits_of(&[("models.worker.model", json!("large"))])),
        );
        assert_eq!(
            out,
            concat!(
                "name = \"small\"\n",
                "\n",
                "[models.worker]\n",
                "model = \"large\"\n",
            )
        );
    }

    /// Key order is the operator's: a set rewrites one line in place, and a
    /// new key joins its table at the end rather than at the top.
    #[test]
    fn key_order_survives_a_set() {
        let out = want_written(nested_pane(HEADER).edited_section_with(&edits_of(&[
            ("models.worker.token", json!("sk-new")),
            ("name", json!("large")),
        ])));
        assert_eq!(
            out,
            concat!(
                "# which model to run\n",
                "name = \"large\"\n",
                "\n",
                "[models.worker]\n",
                "# the credential\n",
                "token = \"sk-new\"\n",
                "model = \"gpt\"\n",
            )
        );
    }

    /// Two edits to one table are two lines of the same write.
    #[test]
    fn two_edits_in_one_table_both_land() {
        let out = want_written(nested_pane(HEADER).edited_section_with(&edits_of(&[
            ("models.worker.model", json!("large")),
            ("models.worker.token", json!("sk-new")),
        ])));
        assert_eq!(
            out,
            concat!(
                "# which model to run\n",
                "name = \"small\"\n",
                "\n",
                "[models.worker]\n",
                "# the credential\n",
                "token = \"sk-new\"\n",
                "model = \"large\"\n",
            )
        );
    }

    /// `null` is the leaf's own removal: its sibling and the table holding
    /// both stay, comment included.
    #[test]
    fn a_null_removes_only_its_leaf() {
        for (section, want) in [
            (
                HEADER,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    "\n",
                    "[models.worker]\n",
                    "# the credential\n",
                    "token = \"sk-old\"\n",
                ),
            ),
            (
                DOTTED,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    "models.worker.token = \"sk-old\"\n",
                ),
            ),
            (
                INLINE,
                concat!(
                    "# which model to run\n",
                    "name = \"small\"\n",
                    // The space the removed entry owned goes with it.
                    "models = { worker = { token = \"sk-old\"} }\n",
                ),
            ),
        ] {
            let out = want_written(
                nested_pane(section)
                    .edited_section_with(&edits_of(&[("models.worker.model", Value::Null)])),
            );
            assert_eq!(out, want, "{section}");
        }
    }

    /// A batch that changed nothing sends nothing: the removal found no
    /// parent, so the section it would have sent is the one it was handed.
    /// `Nothing`, not `Refused`: the file already says what the operator asked
    /// for.
    #[test]
    fn a_null_under_an_absent_parent_writes_nothing() {
        assert_eq!(
            nested_pane("name = \"small\"\n")
                .edited_section_with(&edits_of(&[("models.worker.model", Value::Null)])),
            SectionWrite::Nothing
        );
    }

    /// An unset field the operator unsets again is a no-op, not a failure: a
    /// refusal here would drop the sibling edit in the same close along with
    /// it.
    #[test]
    fn a_null_on_a_key_the_section_does_not_carry_keeps_the_rest_of_the_batch() {
        let out = want_written(nested_pane(NO_TOKEN).edited_section_with(&edits_of(&[
            ("models.worker.token", Value::Null),
            ("name", json!("large")),
        ])));
        assert_eq!(
            out,
            concat!(
                "# which model to run\n",
                "name = \"large\"\n",
                "\n",
                "[models.worker]\n",
                "model = \"gpt\"\n",
            )
        );
    }

    /// The same for a parent the section does not carry at all.
    #[test]
    fn a_null_under_an_absent_parent_keeps_the_rest_of_the_batch() {
        let out = want_written(
            nested_pane("name = \"small\"\n").edited_section_with(&edits_of(&[
                ("models.worker.model", Value::Null),
                ("name", json!("large")),
            ])),
        );
        assert_eq!(out, "name = \"large\"\n");
    }

    /// `models = "old"` is the operator's own line, and a row editing a key
    /// inside `models` is not licensed to trade it for a table. Refused whole:
    /// a batch this writer cannot finish sends no part of itself.
    #[test]
    fn a_set_onto_a_scalar_where_the_schema_has_a_table_writes_nothing() {
        for section in [
            "models = \"old\"\nname = \"small\"\n",
            "models.worker = \"old\"\nname = \"small\"\n",
            "models = { worker = \"old\" }\nname = \"small\"\n",
        ] {
            let pane = nested_pane(section);
            assert_eq!(
                pane.edited_section_with(&edits_of(&[("models.worker.model", json!("large"))])),
                SectionWrite::Refused,
                "{section}"
            );
            assert_eq!(
                pane.edited_section_with(&edits_of(&[
                    ("models.worker.model", json!("large")),
                    ("name", json!("large")),
                ])),
                SectionWrite::Refused,
                "{section}"
            );
        }
    }

    /// No row of this pane ends at a table, so this is the writer's own rule
    /// stated where it lives rather than a path an operator reaches.
    #[test]
    fn a_set_never_replaces_a_table_the_operator_wrote() {
        let mut doc: toml_edit::DocumentMut = "[models.worker]\nmodel = \"gpt\"\n"
            .parse()
            .expect("the section parses");
        let path = [PathStep::Key("models".to_owned())];
        assert_eq!(
            set_at(doc.as_table_mut(), &path, 0, toml_edit::value("old"), false),
            Landed::Refused
        );
        assert!(doc.to_string().contains("[models.worker]"), "{doc}");
    }

    /// A new table on the path takes the form of the table it goes into.
    /// Inserting a regular `Item::Table` into an inline table reports success
    /// and renders nothing at all: inline tables emit only their values, so
    /// the section comes back exactly as it went in and the edit is gone.
    #[test]
    fn a_set_inside_an_inline_table_creates_an_inline_table() {
        for (section, want) in [
            (
                "models = {}\nname = \"small\"\n",
                "models = { worker = { model = \"large\" } }\nname = \"small\"\n",
            ),
            (
                "models = { token = \"sk-old\" }\nname = \"small\"\n",
                // The space the new entry's separator takes sits before the
                // comma rather than after it, which is `toml_edit` keeping the
                // decor the operator's own entry had.
                concat!(
                    "models = { token = \"sk-old\" , worker = { model = \"large\" } }\n",
                    "name = \"small\"\n",
                ),
            ),
            (
                "models = { worker = {} }\nname = \"small\"\n",
                "models = { worker = { model = \"large\" } }\nname = \"small\"\n",
            ),
            (
                "models = { worker = { token = \"sk-old\" } }\nname = \"small\"\n",
                concat!(
                    "models = { worker = { token = \"sk-old\" , model = \"large\" } }\n",
                    "name = \"small\"\n",
                ),
            ),
        ] {
            let out = want_written(
                nested_pane(section)
                    .edited_section_with(&edits_of(&[("models.worker.model", json!("large"))])),
            );
            assert_eq!(out, want, "{section}");
            let doc: toml_edit::DocumentMut = out.parse().expect("the output parses");
            assert_eq!(
                doc["models"]["worker"]["model"].as_str(),
                Some("large"),
                "{out}"
            );
        }
    }

    /// A property literally named `a.b` is one key, and the section spells
    /// it quoted. The row key carries the quotes `segment` added to it.
    #[test]
    fn a_property_named_with_a_dot_stays_one_key() {
        let pane = nested_pane("\"a.b\" = \"literal\"\nname = \"small\"\n");
        assert_eq!(pane.value("\"a.b\""), "literal");
        let out = want_written(pane.edited_section_with(&edits_of(&[("\"a.b\"", json!("other"))])));
        assert_eq!(out, "\"a.b\" = \"other\"\nname = \"small\"\n");
    }

    /// A number this pane cannot name is refused rather than rounded: a
    /// `u64` above `i64::MAX` reaches `toml_edit` as a float, and a rounded
    /// number in `dogs.toml` is a wrong setting the dog reads back as valid.
    #[test]
    fn a_number_that_is_neither_an_integer_nor_a_float_writes_nothing() {
        let pane = nested_pane("name = \"small\"\n");
        assert_eq!(
            pane.edited_section_with(&edits_of(&[("name", json!(u64::MAX))])),
            SectionWrite::Refused
        );
    }

    /// An array that is not all strings or all integers has no TOML list the
    /// pane edits, and a map has no cell at all. JSON text written as a TOML
    /// string is a value the dog cannot read back as the shape it was.
    #[test]
    fn a_mixed_array_an_array_of_objects_or_a_map_writes_nothing() {
        let pane = nested_pane("name = \"small\"\n");
        for value in [
            json!(["a", 1]),
            json!([1.5, 2.5]),
            json!([true]),
            json!([null]),
            json!([["a"]]),
            json!([{ "model": "gpt" }]),
            json!({ "model": "gpt" }),
        ] {
            assert_eq!(
                pane.edited_section_with(&edits_of(&[("name", value.clone())])),
                SectionWrite::Refused,
                "{value}"
            );
        }
    }

    /// The array replaces the operator's line where it stands, and the
    /// comment above it is the operator's own.
    #[test]
    fn a_string_list_replaces_its_line_and_keeps_the_comment_above_it() {
        let out = want_written(
            list_pane("# who to ask\nhosts = [\"a\"]\nname = \"small\"\n")
                .edited_section_with(&edits_of(&[("hosts", json!(["a", "b"]))])),
        );
        assert_eq!(
            out,
            "# who to ask\nhosts = [\"a\", \"b\"]\nname = \"small\"\n"
        );
    }

    #[test]
    fn an_integer_list_writes_integers_not_text() {
        let out = want_written(
            list_pane("codes = [0]\nname = \"small\"\n")
                .edited_section_with(&edits_of(&[("codes", json!([0, 143]))])),
        );
        assert_eq!(out, "codes = [0, 143]\nname = \"small\"\n");
    }

    /// Removing the last element leaves the key set to nothing, which is not
    /// the same as unsetting it.
    #[test]
    fn an_empty_list_writes_an_empty_array() {
        let out = want_written(
            list_pane("hosts = [\"a\"]\n").edited_section_with(&edits_of(&[("hosts", json!([]))])),
        );
        assert_eq!(out, "hosts = []\n");
    }

    /// A list the section does not carry yet joins it at the end.
    #[test]
    fn a_list_the_section_lacks_is_added_after_the_keys_it_has() {
        let out = want_written(
            list_pane("name = \"small\"\n")
                .edited_section_with(&edits_of(&[("hosts", json!(["a"]))])),
        );
        assert_eq!(out, "name = \"small\"\nhosts = [\"a\"]\n");
    }

    /// A list one table down is written at its dotted path, in whichever of
    /// the three forms the section uses for the table.
    #[test]
    fn a_list_under_a_nested_table_lands_at_its_dotted_path() {
        for (section, want) in [
            (
                "[models.worker]\n# the pool\nmodels = [\"gpt\"]\n",
                "[models.worker]\n# the pool\nmodels = [\"gpt\", \"large\"]\n",
            ),
            (
                "models.worker.models = [\"gpt\"]\n",
                "models.worker.models = [\"gpt\", \"large\"]\n",
            ),
            (
                "[models.worker]\nmodel = \"gpt\"\n",
                "[models.worker]\nmodel = \"gpt\"\nmodels = [\"gpt\", \"large\"]\n",
            ),
        ] {
            let out = want_written(list_pane(section).edited_section_with(&edits_of(&[(
                "models.worker.models",
                json!(["gpt", "large"]),
            )])));
            assert_eq!(out, want, "{section}");
        }
    }

    /// The four shapes the writer does have an arm for, each in its own TOML
    /// type rather than as text.
    #[test]
    fn a_scalar_writes_its_own_toml_type() {
        for (value, want) in [
            (json!(-5), "name = -5\n"),
            (json!(1.5), "name = 1.5\n"),
            (json!(true), "name = true\n"),
            (json!("gpt"), "name = \"gpt\"\n"),
        ] {
            let pane = nested_pane("name = 0\n");
            let out = want_written(pane.edited_section_with(&edits_of(&[("name", value.clone())])));
            assert_eq!(out, want, "{value}");
        }
    }

    /// Re-rendering from parsed values would delete every comment in a file
    /// shep does not author.
    #[test]
    fn an_edited_section_keeps_its_comments_and_changes_one_key() {
        let pane = bark_pane();
        let mut edits = Edits::default();
        edits.set(field("poll", json!("30s")), None);
        let out = want_written(pane.edited_section_with(&edits));
        assert!(out.contains("# how often"), "{out}");
        assert!(out.contains("poll = \"30s\""), "{out}");
        assert!(out.contains("history_bytes = 4096"), "{out}");
        assert!(out.contains("url = \"https://hooks.example/x\""), "{out}");
    }

    /// An empty buffer unsets the key, putting it back on the dog's own
    /// default.
    #[test]
    fn a_null_edit_removes_the_key_from_the_section() {
        let pane = bark_pane();
        let mut edits = Edits::default();
        edits.set(field("history_bytes", Value::Null), None);
        let out = want_written(pane.edited_section_with(&edits));
        assert!(!out.contains("history_bytes"), "{out}");
        assert!(out.contains("# how often"), "{out}");
    }

    /// A dog section takes every filed edit in one write rather than one
    /// write per edit.
    #[test]
    fn a_dog_section_takes_every_filed_edit_in_one_write() {
        let pane = bark_pane();
        let mut edits = Edits::default();
        edits.set(field("sink_timeout", json!("5s")), None);
        edits.set(field("poll", json!("30s")), None);
        let section = want_written(pane.edited_section_with(&edits));
        assert!(section.contains("poll = \"30s\""), "{section}");
        assert!(section.contains("sink_timeout = \"5s\""), "{section}");
    }

    /// An env edit has no home in a dog's section and must not silently
    /// become a key in it.
    #[test]
    fn an_env_edit_is_ignored_by_a_dog_section() {
        let pane = bark_pane();
        let mut edits = Edits::default();
        edits.set(
            PaneEdit::SetEnv {
                key: "SECRET".to_owned(),
                value: None,
            },
            None,
        );
        assert_eq!(pane.edited_section_with(&edits), SectionWrite::Nothing);
    }

    /// A stray env edit in a set that also carries a field edit must not
    /// revive the old "one bad entry aborts the whole batch" bug: skipping
    /// the env edit and an early return on any non-field entry both leave
    /// `an_env_edit_is_ignored_by_a_dog_section` green, since an env-only
    /// set answers `Nothing` either way. Only a mixed set tells them apart.
    #[test]
    fn a_field_edit_lands_even_when_the_same_batch_carries_an_env_edit() {
        let pane = bark_pane();
        let mut edits = Edits::default();
        edits.set(field("poll", json!("30s")), None);
        edits.set(
            PaneEdit::SetEnv {
                key: "SECRET".to_owned(),
                value: None,
            },
            None,
        );
        let out = want_written(pane.edited_section_with(&edits));
        assert!(out.contains("poll = \"30s\""), "{out}");
        assert!(!out.contains("SECRET"), "{out}");
    }

    /// A section here would send a sheep's config out through a dog's door,
    /// and there is no section to send it from.
    #[test]
    fn a_sheep_pane_has_no_section_to_edit() {
        let pane = ConfigPane::sheep(web());
        let mut edits = Edits::default();
        edits.set(field("cwd", json!("/srv")), None);
        assert_eq!(pane.edited_section_with(&edits), SectionWrite::Nothing);
    }
}