hennery 0.3.1

Self-hosted cockpit for ACP coding agents
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
//! The service commands against a fake service manager, in temporary
//! directories: no test here runs `launchctl`, `systemctl` or `loginctl`,
//! or touches a real home directory.

use super::*;
use std::cell::RefCell;

/// How `Fake` answers a command line it was asked `n` times before.
type Answer = Box<dyn Fn(&str, usize) -> Ran>;

/// Records every command and answers it with `answer`.
struct Fake {
    calls: RefCell<Vec<String>>,
    answer: Answer,
}

impl Fake {
    /// `answer(command line, how many times it was asked before)`.
    fn new(answer: impl Fn(&str, usize) -> Ran + 'static) -> Self {
        Self {
            calls: RefCell::default(),
            answer: Box::new(answer),
        }
    }

    fn calls(&self) -> Vec<String> {
        self.calls.borrow().clone()
    }
}

impl Manager for Fake {
    fn run(&self, program: &str, args: &[&str]) -> Result<Ran> {
        let line = std::iter::once(program)
            .chain(args.iter().copied())
            .collect::<Vec<_>>()
            .join(" ");
        let before = self.calls.borrow().iter().filter(|c| **c == line).count();
        self.calls.borrow_mut().push(line.clone());
        Ok((self.answer)(&line, before))
    }
}

fn ok(stdout: &str) -> Ran {
    Ran {
        ok: true,
        stdout: stdout.into(),
        stderr: String::new(),
    }
}

fn failed(stderr: &str) -> Ran {
    Ran {
        ok: false,
        stdout: String::new(),
        stderr: stderr.into(),
    }
}

/// A machine in `dir`: its home, its `/` (for `/run` and `/proc`), this
/// test binary as the one to install, and `/bin/sh` as the login shell.
fn machine<'a>(dir: &Path, platform: Platform, manager: &'a Fake) -> Context<'a> {
    let home = dir.join("home");
    std::fs::create_dir_all(&home).unwrap();
    let root = dir.join("root");
    std::fs::create_dir_all(&root).unwrap();
    Context {
        platform,
        env: BTreeMap::from([("HOME".to_string(), home.display().to_string())]),
        home,
        uid: 501,
        user: "hennery-test".into(),
        shell: PathBuf::from("/bin/sh"),
        exe: std::env::current_exe().unwrap(),
        root,
        manager,
    }
}

/// A Linux machine with systemd running.
fn linux<'a>(dir: &Path, manager: &'a Fake) -> Context<'a> {
    let cx = machine(dir, Platform::Linux, manager);
    std::fs::create_dir_all(cx.root.join("run/systemd/system")).unwrap();
    cx
}

fn mode(path: &Path) -> u32 {
    std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}

/// A host data directory with a pairing in it.
fn paired(dir: &Path) -> PathBuf {
    let data = dir.join("host-data");
    hennery_host::identity::Paired {
        collector_url: "ws://127.0.0.1:7117/api/hosts/ws".into(),
        trust: Default::default(),
        host_id: "host-test".into(),
        key: hennery_host::identity::HostKey::generate(),
        workspace_roots: Vec::new(),
    }
    .save(&data)
    .unwrap();
    data
}

/// macOS (§6.2): the plist goes to `~/Library/LaunchAgents`, runs `up` on
/// the default data directory, has the captured PATH, and is loaded with
/// `launchctl bootstrap gui/$UID`; the log directory is private.
#[test]
fn installing_on_macos_writes_and_bootstraps_the_agent() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.contains(" print ") {
            failed("not found")
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let mut out = Vec::new();
    install(&cx, Role::Up, None, None, &mut out).unwrap();
    let out = String::from_utf8(out).unwrap();
    let plist = cx.home.join("Library/LaunchAgents/dev.hennery.up.plist");
    assert_eq!(
        fake.calls(),
        [
            "launchctl print gui/501/dev.hennery.up".to_string(),
            format!("launchctl bootstrap gui/501 {}", plist.display()),
        ]
    );
    let text = std::fs::read_to_string(&plist).unwrap();
    let data = cx.home.join("Library/Application Support/hennery");
    let argv = unit::command_line(Role::Up, &cx.exe, &data).unwrap();
    let args: String = argv.iter().map(|a| format!("\t\t<string>{a}</string>\n")).collect();
    assert!(text.contains(&format!("<array>\n{args}\t</array>")), "{text}");
    let (_, path) = text.split_once("<key>PATH</key>\n\t\t<string>").unwrap();
    let (path, _) = path.split_once("</string>").unwrap();
    for dir in ["/usr/bin", "/bin", "/usr/sbin", "/sbin"] {
        assert!(path.split(':').any(|e| e == dir), "{dir} not in {path}");
    }
    assert!(text.contains("<string>Aqua</string>"), "{text}");
    assert_eq!(unit::plist_shell(&text).as_deref(), Some("/bin/sh"), "{text}");
    assert!(out.contains("from the login shell: /bin/sh"), "{out}");
    let logs = cx.home.join("Library/Logs/hennery");
    assert_eq!(mode(&logs), 0o700);
    assert_eq!(mode(&logs.join("up.log")), 0o600);
    assert_eq!(mode(&plist), 0o644);
    assert!(out.contains(&plist.display().to_string()), "{out}");
    assert!(out.contains("PATH (in the plist): "), "{out}");
    assert!(
        out.contains(&format!("hennery admin --data-dir {} setup-url", data.display())),
        "{out}"
    );
    assert!(!out.contains("restarted"), "{out}");
}

/// Reinstalling a loaded agent boots the old one out and waits until
/// launchd has let it go before bootstrapping the new one.
#[test]
fn reinstalling_on_macos_waits_for_the_bootout() {
    let dir = tempfile::tempdir().unwrap();
    // Loaded at first, and for one more look after the bootout.
    let fake = Fake::new(|line, before| {
        if line.contains(" print ") && before < 2 {
            ok("state = running")
        } else if line.contains(" print ") {
            failed("not found")
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let mut out = Vec::new();
    install(&cx, Role::Up, None, None, &mut out).unwrap();
    let calls = fake.calls();
    let bootout = calls.iter().position(|c| c.contains(" bootout ")).unwrap();
    let bootstrap = calls.iter().position(|c| c.contains(" bootstrap ")).unwrap();
    assert_eq!(calls[bootout], "launchctl bootout gui/501/dev.hennery.up");
    assert_eq!(calls[bootout + 1..bootstrap].len(), 2, "{calls:?}");
    assert!(String::from_utf8(out).unwrap().contains("restarted"));
}

/// No GUI session (over SSH only): the error says what a launchd user agent
/// needs.
#[test]
fn a_failed_bootstrap_explains_the_gui_session() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.contains(" bootstrap ") {
            failed("Bootstrap failed: 125: Domain does not support specified action")
        } else {
            failed("not found")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let err = install(&cx, Role::Up, None, None, &mut Vec::new()).unwrap_err();
    let err = format!("{err:#}");
    assert!(err.contains("GUI login session") && err.contains("125"), "{err}");
}

/// Linux (§6.3): the unit and its environment file are written, the unit
/// enabled and (re)started, and linger is checked but only advised.
#[test]
fn installing_on_linux_writes_enables_and_starts_the_unit() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.starts_with("loginctl ") {
            ok("Linger=no\n")
        } else if line.contains("is-active") {
            failed("")
        } else {
            ok("")
        }
    });
    let mut cx = linux(dir.path(), &fake);
    let config = dir.path().join("xdg-config");
    cx.env.insert("XDG_CONFIG_HOME".into(), config.display().to_string());
    let data = paired(dir.path());
    let mut out = Vec::new();
    install(&cx, Role::Host, Some(&data), None, &mut out).unwrap();
    let out = String::from_utf8(out).unwrap();
    assert_eq!(
        fake.calls(),
        [
            "systemctl --user is-active --quiet hennery-host.service",
            "systemctl --user daemon-reload",
            "systemctl --user enable hennery-host.service",
            "systemctl --user reset-failed hennery-host.service",
            "systemctl --user restart hennery-host.service",
            "loginctl show-user 501 -p Linger",
        ]
    );
    let unit_file = config.join("systemd/user/hennery-host.service");
    let text = std::fs::read_to_string(&unit_file).unwrap();
    let exec = format!(
        "ExecStart=\"{}\" \"host\" \"run\" \"--data-dir\" \"{}\"\n",
        cx.exe.display(),
        data.display()
    );
    assert!(text.contains(&exec), "{text}");
    let env_file = config.join("hennery/service.env");
    assert!(
        text.contains(&format!("EnvironmentFile={}\n", env_file.display())),
        "{text}"
    );
    assert!(text.contains("KillMode=mixed"), "{text}");
    assert_eq!(mode(&env_file), 0o600);
    let env = std::fs::read_to_string(&env_file).unwrap();
    let line = env.lines().last().unwrap();
    assert!(line.starts_with("PATH=\"/") && line.ends_with('"'), "{env}");
    assert!(line.split([':', '"']).any(|e| e == "/usr/bin"), "{env}");
    assert!(!env.contains("CLAUDE"), "{env}");
    assert_eq!(unit::env_file_shell(&env).as_deref(), Some("/bin/sh"), "{env}");
    assert!(out.contains("loginctl enable-linger hennery-test"), "{out}");
    assert!(out.contains(&format!("PATH (in {})", env_file.display())), "{out}");
}

/// `--shell` is the shell the PATH is captured from, and the one recorded;
/// one that is not an absolute path is refused before it is run.
#[test]
fn the_shell_given_is_recorded_and_must_be_absolute() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.contains(" print ") {
            failed("not found")
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let shell = crate::service::path::marker_shell(dir.path());
    let mut out = Vec::new();
    install(&cx, Role::Up, None, Some(&shell), &mut out).unwrap();
    let plist = std::fs::read_to_string(cx.service_file(Role::Up)).unwrap();
    assert_eq!(unit::plist_shell(&plist).as_deref(), shell.to_str(), "{plist}");
    let marker = dir.path().join("marker-bin");
    let path = unit::plist_path(&plist).unwrap();
    assert!(path.split(':').any(|e| Path::new(e) == marker), "{plist}");
    let out = String::from_utf8(out).unwrap();
    assert!(
        out.contains(&format!("from the login shell: {}", shell.display())),
        "{out}"
    );

    // The same script, named relative to this process's directory: it would
    // run if it were tried.
    let ran = dir.path().join("marker-ran");
    std::fs::remove_file(&ran).unwrap();
    let cwd = std::env::current_dir().unwrap();
    let up = "../".repeat(cwd.components().count() - 1);
    let relative = Path::new(&up).join(shell.strip_prefix("/").unwrap());
    assert!(relative.is_relative() && relative.is_file(), "{}", relative.display());
    let before = fake.calls().len();
    let err = install(&cx, Role::Up, None, Some(&relative), &mut Vec::new()).unwrap_err();
    assert!(format!("{err:#}").contains("--shell"), "{err:#}");
    assert!(!ran.exists(), "the relative shell was run");
    assert_eq!(fake.calls().len(), before, "nothing runs for a relative shell");
}

/// Linger on: nothing to advise.
#[test]
fn linger_on_is_not_mentioned() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.starts_with("loginctl ") {
            ok("Linger=yes\n")
        } else {
            ok("")
        }
    });
    let cx = linux(dir.path(), &fake);
    let mut out = Vec::new();
    install(&cx, Role::Collector, None, None, &mut out).unwrap();
    let out = String::from_utf8(out).unwrap();
    assert!(!out.contains("linger"), "{out}");
    assert!(out.contains("restarted"), "{out}");
    let unit_file = cx.home.join(".config/systemd/user/hennery-collector.service");
    let text = std::fs::read_to_string(unit_file).unwrap();
    let data = cx.home.join(".local/share/hennery");
    let exec = format!("\"collector\" \"--data-dir\" \"{}\"\n", data.display());
    assert!(text.contains(&exec), "{text}");
}

/// A host data directory with no pairing would crash-loop: refused before
/// anything is written or run.
#[test]
fn a_host_without_a_pairing_is_refused() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok(""));
    let cx = linux(dir.path(), &fake);
    let empty = dir.path().join("empty");
    let err = install(&cx, Role::Host, Some(&empty), None, &mut Vec::new()).unwrap_err();
    assert!(err.to_string().contains("hennery host join"), "{err}");
    assert!(fake.calls().is_empty());
    assert!(!cx.home.join(".config").exists());

    // A pairing left half-done is refused too, and left as it is: putting
    // it in place is the host's, under its lock (decision 8).
    let data = paired(dir.path());
    let pending = data.join("host.toml.pending");
    std::fs::write(&pending, "staged").unwrap();
    let err = install(&cx, Role::Host, Some(&data), None, &mut Vec::new()).unwrap_err();
    assert!(err.to_string().contains("was interrupted"), "{err}");
    assert_eq!(std::fs::read_to_string(&pending).unwrap(), "staged");
    assert!(fake.calls().is_empty());
}

/// Distribution spec §7 check 10: one role per machine.
#[test]
fn a_second_role_is_refused() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok(""));
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let up = cx.service_file(Role::Up);
    std::fs::create_dir_all(up.parent().unwrap()).unwrap();
    std::fs::write(&up, "").unwrap();
    let err = install(&cx, Role::Collector, None, None, &mut Vec::new()).unwrap_err();
    assert!(err.to_string().contains("uninstall --role up"), "{err}");
    assert!(fake.calls().is_empty());
}

/// No systemd: on WSL the error says how to turn it on; elsewhere that
/// there is none.
#[test]
fn without_systemd_install_says_why() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok(""));
    let cx = machine(dir.path(), Platform::Linux, &fake);
    let err = install(&cx, Role::Up, None, None, &mut Vec::new()).unwrap_err();
    assert!(err.to_string().contains("systemd is not running"), "{err}");
    std::fs::create_dir_all(cx.root.join("proc/sys/kernel")).unwrap();
    std::fs::write(
        cx.root.join("proc/sys/kernel/osrelease"),
        "5.15.153.1-microsoft-standard-WSL2\n",
    )
    .unwrap();
    let err = install(&cx, Role::Up, None, None, &mut Vec::new()).unwrap_err();
    assert!(err.to_string().contains("/etc/wsl.conf"), "{err}");
    assert!(fake.calls().is_empty());
}

/// A login shell that fails is an error naming `--shell`, not a silent
/// fallback to some other PATH (D-3).
#[test]
fn a_failing_login_shell_fails_the_install() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok(""));
    let cx = linux(dir.path(), &fake);
    let err = install(&cx, Role::Up, None, Some(Path::new("/usr/bin/false")), &mut Vec::new()).unwrap_err();
    assert!(format!("{err:#}").contains("--shell"), "{err:#}");
    assert!(fake.calls().is_empty());
    assert!(!cx.service_file(Role::Up).exists());
}

/// Uninstall stops and removes the unit and its environment file, and
/// keeps the data directory.
#[test]
fn uninstalling_on_linux_removes_the_unit_and_keeps_the_data() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok("Linger=yes"));
    let cx = linux(dir.path(), &fake);
    install(&cx, Role::Up, None, None, &mut Vec::new()).unwrap();
    std::fs::create_dir_all(cx.default_data_dir()).unwrap();
    fake.calls.borrow_mut().clear();
    let mut out = Vec::new();
    uninstall(&cx, None, &mut out).unwrap();
    assert_eq!(
        fake.calls(),
        [
            "systemctl --user disable --now hennery.service",
            "systemctl --user daemon-reload"
        ]
    );
    assert!(!cx.service_file(Role::Up).exists());
    assert!(!cx.env_file().exists());
    assert!(cx.default_data_dir().exists());
    let out = String::from_utf8(out).unwrap();
    assert!(out.contains("data directory is kept"), "{out}");
    let mut again = Vec::new();
    uninstall(&cx, None, &mut again).unwrap();
    assert!(
        String::from_utf8(again)
            .unwrap()
            .contains("no hennery service is installed")
    );
}

#[test]
fn uninstalling_on_macos_boots_out_and_removes_the_plist() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, before| {
        if line.contains(" print ") && before == 0 {
            ok("state = running")
        } else if line.contains(" print ") {
            failed("not found")
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let plist = cx.service_file(Role::Host);
    std::fs::create_dir_all(plist.parent().unwrap()).unwrap();
    std::fs::write(
        &plist,
        unit::plist(Role::Host, &["/x".into()], "/usr/bin", "/bin/sh", "/tmp/l"),
    )
    .unwrap();
    let mut out = Vec::new();
    uninstall(&cx, Some(Role::Host), &mut out).unwrap();
    assert!(
        fake.calls()
            .contains(&"launchctl bootout gui/501/dev.hennery.host".to_string())
    );
    assert!(!plist.exists());
    let out = String::from_utf8(out).unwrap();
    assert!(out.contains("its log is kept"), "{out}");
}

/// `service.env` is every role's: with two roles installed (which `status`
/// warns about), removing one keeps it for the other.
#[test]
fn uninstalling_one_of_two_roles_keeps_the_env_file() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|_, _| ok("Linger=yes"));
    let cx = linux(dir.path(), &fake);
    install(&cx, Role::Collector, None, None, &mut Vec::new()).unwrap();
    let up = cx.service_file(Role::Up);
    std::fs::write(&up, "").unwrap();
    uninstall(&cx, Some(Role::Up), &mut Vec::new()).unwrap();
    assert!(cx.env_file().exists());
    uninstall(&cx, Some(Role::Collector), &mut Vec::new()).unwrap();
    assert!(!cx.env_file().exists());
}

/// Status names the installed role, what it runs, what launchd says, and
/// `up`'s children; a child given up on makes it exit 1.
#[test]
fn status_reports_the_service_and_ups_children() {
    let dir = tempfile::tempdir().unwrap();
    // This test's own process stands in for `up`, so the report is live.
    let pid = std::process::id();
    let fake = Fake::new(move |line, _| {
        if line.contains(" print ") {
            ok(&format!(
                "gui/501/dev.hennery.up = {{\n\tstate = running\n\tpid = {pid}\n}}"
            ))
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let data = dir.path().join("data");
    std::fs::create_dir_all(&data).unwrap();
    let argv = unit::command_line(Role::Up, &cx.exe, &data).unwrap();
    let plist = cx.service_file(Role::Up);
    std::fs::create_dir_all(plist.parent().unwrap()).unwrap();
    std::fs::write(&plist, unit::plist(Role::Up, &argv, "/usr/bin", "/bin/sh", "/tmp/l")).unwrap();
    let report = |state| supervisor::ChildReport {
        state,
        crashes_in_window: 10,
        restarts: 9,
        last_exit: Some("exit status: 1".into()),
    };
    let mut state = supervisor::State {
        pid,
        updated_at: 0,
        collector: report(supervisor::ChildState::Running),
        host: report(supervisor::ChildState::Running),
    };
    supervisor::write_state(&data, &state).unwrap();
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::SUCCESS);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains(&format!("launchd: running, pid {pid}")), "{text}");
    assert!(text.contains(&format!("runs: {}", argv.join(" "))), "{text}");

    state.host = report(supervisor::ChildState::GaveUp);
    supervisor::write_state(&data, &state).unwrap();
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::FAILURE);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains("host: given up on after 10 crashes"), "{text}");

    // Written by a process that is gone: stale, and judged by nothing.
    let mut gone = std::process::Command::new("/usr/bin/true").spawn().unwrap();
    gone.wait().unwrap();
    state.pid = gone.id();
    supervisor::write_state(&data, &state).unwrap();
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::SUCCESS);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains("the last report is stale"), "{text}");
    assert!(!text.contains("given up on"), "{text}");
}

/// Either condition alone makes the report stale: a live pid that is not
/// the one launchd names, or a dead pid when launchd names none. And a
/// report that cannot be read is said, and fails `status`.
#[test]
fn a_report_is_stale_by_either_condition_and_an_unreadable_one_fails() {
    let dir = tempfile::tempdir().unwrap();
    let launchd_pid = std::rc::Rc::new(std::cell::Cell::new(None::<u32>));
    let named = launchd_pid.clone();
    let fake = Fake::new(move |line, _| {
        if line.contains(" print ") {
            match named.get() {
                Some(pid) => ok(&format!("state = running\npid = {pid}\n")),
                None => ok("state = running\n"),
            }
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let data = dir.path().join("data");
    std::fs::create_dir_all(&data).unwrap();
    let argv = unit::command_line(Role::Up, &cx.exe, &data).unwrap();
    let plist = cx.service_file(Role::Up);
    std::fs::create_dir_all(plist.parent().unwrap()).unwrap();
    std::fs::write(&plist, unit::plist(Role::Up, &argv, "/usr/bin", "/bin/sh", "/tmp/l")).unwrap();
    let gave_up = supervisor::ChildReport {
        state: supervisor::ChildState::GaveUp,
        crashes_in_window: 10,
        restarts: 9,
        last_exit: None,
    };
    let report = |pid| supervisor::State {
        pid,
        updated_at: 0,
        collector: gave_up.clone(),
        host: gave_up.clone(),
    };
    let stale = |cx: &Context| {
        let mut out = Vec::new();
        let code = status(cx, None, &mut out).unwrap();
        let text = String::from_utf8(out).unwrap();
        (code, text.contains("the last report is stale"), text)
    };

    // Alive, but not the process launchd runs.
    supervisor::write_state(&data, &report(std::process::id())).unwrap();
    launchd_pid.set(Some(1));
    let (code, is_stale, text) = stale(&cx);
    assert!(is_stale, "{text}");
    assert_eq!(code, ExitCode::SUCCESS, "{text}");

    // launchd names no pid, and the report's is gone.
    let mut gone = std::process::Command::new("/usr/bin/true").spawn().unwrap();
    gone.wait().unwrap();
    supervisor::write_state(&data, &report(gone.id())).unwrap();
    launchd_pid.set(None);
    let (_, is_stale, text) = stale(&cx);
    assert!(is_stale, "{text}");

    // Neither: the report counts, and its give-up fails `status`.
    supervisor::write_state(&data, &report(std::process::id())).unwrap();
    let (code, is_stale, text) = stale(&cx);
    assert!(!is_stale, "{text}");
    assert_eq!(code, ExitCode::FAILURE, "{text}");

    std::fs::write(data.join(supervisor::STATE_FILE), "{not json").unwrap();
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::FAILURE);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains("the report cannot be read"), "{text}");
}

/// Status: nothing installed, a missing binary, a unit that is not active.
#[test]
fn status_fails_when_the_service_cannot_run() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.contains("is-active") {
            ok("failed\n")
        } else if line.contains("is-enabled") {
            ok("enabled\n")
        } else {
            ok("Linger=yes")
        }
    });
    let cx = linux(dir.path(), &fake);
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::FAILURE);
    assert!(
        String::from_utf8(out)
            .unwrap()
            .contains("no hennery service is installed")
    );
    assert!(fake.calls().is_empty());

    let argv = unit::command_line(
        Role::Collector,
        Path::new("/nonexistent/hennery"),
        &cx.default_data_dir(),
    )
    .unwrap();
    let file = cx.service_file(Role::Collector);
    std::fs::create_dir_all(file.parent().unwrap()).unwrap();
    std::fs::write(&file, unit::systemd_unit(Role::Collector, &argv, "/tmp/env").unwrap()).unwrap();
    let mut out = Vec::new();
    assert_eq!(status(&cx, None, &mut out).unwrap(), ExitCode::FAILURE);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains("its binary is missing"), "{text}");
    assert!(text.contains("systemd: failed, enabled"), "{text}");
}

/// `service status` for a host service whose host was revoked: launchd's
/// revoked host exits 0 and is not loaded or running, so the record says
/// why, with the re-pair and how to start it, and the status fails; a
/// record a re-pair retired says nothing.
#[test]
fn status_says_a_host_service_was_revoked_from_its_record() {
    let dir = tempfile::tempdir().unwrap();
    let fake = Fake::new(|line, _| {
        if line.contains(" print ") {
            failed("Could not find service")
        } else {
            ok("")
        }
    });
    let cx = machine(dir.path(), Platform::MacOs, &fake);
    let data = paired(dir.path());
    let plist = cx.service_file(Role::Host);
    std::fs::create_dir_all(plist.parent().unwrap()).unwrap();
    let argv = unit::command_line(Role::Host, &cx.exe, &data).unwrap();
    std::fs::write(&plist, unit::plist(Role::Host, &argv, "/usr/bin", "/bin/sh", "/tmp/l")).unwrap();
    // The plist keeps restarting on a failure: a revoke exits 0 to stop it,
    // rather than restarting only on a crash, which would also stop the
    // restart after a network failure at start.
    assert!(
        std::fs::read_to_string(&plist)
            .unwrap()
            .contains("<key>SuccessfulExit</key>\n\t\t<false/>")
    );
    let record = |host_id: &str| {
        crate::revoked::record(
            &data,
            &crate::revoked::Revoked {
                host_id: host_id.into(),
                collector_url: "ws://127.0.0.1:7117/api/hosts/ws".into(),
                at: 1,
            },
        )
        .unwrap()
    };
    record("host-test");
    let mut out = Vec::new();
    assert_eq!(status(&cx, Some(Role::Host), &mut out).unwrap(), ExitCode::FAILURE);
    let text = String::from_utf8(out).unwrap();
    assert!(text.contains("revoked by the collector"), "{text}");
    assert!(text.contains("hennery host join http://127.0.0.1:7117"), "{text}");
    assert!(
        text.contains("launchctl kickstart -k gui/501/dev.hennery.host"),
        "{text}"
    );

    record("an-earlier-host");
    let mut out = Vec::new();
    status(&cx, Some(Role::Host), &mut out).unwrap();
    let text = String::from_utf8(out).unwrap();
    assert!(!text.contains("revoked by the collector"), "{text}");
}