trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
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
//! Shared hermetic test fixtures: temp directories (#3382) and a
//! guaranteed-dead loopback address (#4306/#4415).
//!
//! Why (dead loopback): see [`dead_loopback_url`] and [`DEAD_LOOPBACK_PORT`].
//! It lives here, beside `hermetic_temp_dir`, because it is the same kind of
//! thing — a fixture whose whole job is to remove an ambient dependency from
//! the tests that use it — and because three separate copies of it had already
//! drifted across the `tui` test modules.
//!
//! Why (temp dirs): bare `tempfile::TempDir::new()` resolves via `std::env::temp_dir()`,
//! which honors an inherited `$TMPDIR`. A test-invoking harness/sandbox set
//! `TMPDIR` to a real project directory (observed: `~/trusty-mpm-projects`),
//! so every bare `TempDir::new()` call in the suite deposited its mktemp
//! scaffold directly into that project tree instead of a scratch location —
//! 50 directories / ~167MB accumulated over ~24h with nothing reaping them.
//! What: [`hermetic_temp_dir`] is the one replacement for every bare
//! `TempDir::new()` in trusty-mpm's test code. It roots the directory under
//! [`real_system_tmp`] — the hardcoded OS temp path, chosen deliberately
//! over `std::env::temp_dir()` so an inherited `TMPDIR` can never redirect
//! test litter into a project tree again — and tags it with the
//! [`TEST_DIR_PREFIX`] so any directory that survives a hard-killed test
//! process (Drop-based cleanup cannot run after SIGKILL) is trivially
//! attributable and safe to sweep. [`sweep_stale_test_dirs`] runs once per
//! test process (via [`std::sync::Once`], triggered by the first
//! `hermetic_temp_dir()` call) and best-effort removes `tm-test-*`
//! directories older than a day, bounding leak growth without a background
//! daemon.
//! Test: `test_support::tests` below, including regression coverage for a
//! degenerate-`$HOME` false positive (`HOME=/tmp` or `HOME=/`, as seen in
//! root-container / OpenShift-style environments) that an earlier revision
//! of [`guard_against_project_tree`] panicked on unconditionally; see also
//! the TMPDIR-pollution proof in the #3382 PR description
//! (`TMPDIR=$HOME/... cargo test -p trusty-mpm provisioner` deposits nothing
//! under that tree).

use std::path::{Path, PathBuf};
use std::sync::Once;
use std::time::{Duration, SystemTime};

use tempfile::TempDir;

/// RAII ownership of a real tmux session created by a test (#6116).
///
/// The same file backs the `tm` binary's copy of this module — see its own
/// docs for why one source file serves both targets.
///
/// A `cargo test` filter matches the compiled module path, not the source
/// file's basename (#7866). This module compiles as `test_support::tmux_session`
/// even though its source lives in `test_tmux_session.rs`, so `cargo test -p
/// trusty-mpm --lib test_tmux_session` filters against a path that does not
/// exist and silently returns `0 passed; 0 failed; 7361 filtered out` —
/// green, but nothing ran. Filter on `tmux_session` instead: `cargo test -p
/// trusty-mpm --lib tmux_session`.
#[path = "test_tmux_session.rs"]
pub(crate) mod tmux_session;

/// The spawn primitive [`tmux_session`] runs every tmux invocation through
/// (#7060).
///
/// Why: an undisclaimed tmux server takes its macOS TCC responsible process
/// from the signed binary that forked it, so the operator is asked "tmux needs
/// permission to access data from other apps" once per test run.
/// [`crate::core::spawn_disclaim::disclaimed_output`] is the same primitive
/// [`crate::core::tmux::run_tmux_with_bin`] uses for every production spawn.
/// What: a re-export, so the fixture's spawn IS the primitive by construction
/// rather than by a value some later edit could rebind. It lives here, not in
/// the shared fixture file, because that file compiles into both the lib and
/// the `tm` binary and the primitive's path has no spelling valid in both.
/// Test: `tmux_session::tests::the_fixture_spawn_seam_captures_output`.
pub(crate) use crate::core::spawn_disclaim::disclaimed_output as tmux_spawn;

/// This target's spelling of the one lock `$PATH` mutation and PATH-resolved
/// spawns share (#7996).
///
/// Why: [`tmux_session`] execs a bare `tmux`, which the OS resolves through
/// `$PATH`; a concurrent `set_var("PATH", …)` in another test can tear that
/// read. This crate's PATH mutators — `core::git_identity`,
/// `core::gh_account_enforce`, `runtime::claude_code_tests` — already
/// serialise on `core::trusty_tools_config::env_test_lock`, so joining that
/// regime is a re-export rather than a second mutex. The `tm` binary has no
/// visibility of it (`#[cfg(test)] pub(crate)`) and defines its own under this
/// same name; the shared fixture file says `super::lock_path_env()` and
/// compiles into both.
/// Test: `tmux_session::tests::the_spawn_seam_takes_the_path_lock`.
pub(crate) use crate::core::trusty_tools_config::env_test_lock as lock_path_env;

/// Arm `core::home_write_fence` for the lib test binary, before `main` (#8545).
///
/// Why: the lib target wrote `~/.claude/settings.json`, `~/.claude.json` and
/// `~/.trusty-mpm/{sessions,usage,projects}` during `cargo test -p trusty-mpm`.
/// The `tm` bin target arms the same fence from its own `test_support`.
/// What: a pre-`main` constructor, so the roots are recorded before libtest
/// starts a test thread. A test that later repoints `$HOME` to a temp dir
/// writes there freely; only the homes seen at startup are fenced.
/// Test: `tests::the_home_write_fence_is_armed_for_the_lib_binary`.
#[ctor::ctor]
fn arm_home_write_fence() {
    crate::core::home_write_fence::arm_for_this_process();
}

/// Give the lib test binary its own default tmux server, before `main` (#6542).
///
/// Aborts the binary when the private directory cannot be created, rather than
/// let a test reach the operator's server. See `core::tmux_test_isolation`.
/// Test: `core::tmux_test_isolation::tests::this_test_binary_runs_on_a_relocated_tmux_server`.
#[ctor::ctor]
fn isolate_tmux_server() {
    crate::core::tmux_test_isolation::isolate_for_this_process()
        .expect("#6542: create this test binary's private tmux directory");
}

/// Kill the private tmux servers and remove their directory at exit (#6542).
#[ctor::dtor]
fn teardown_tmux_server() {
    crate::core::tmux_test_isolation::teardown_for_this_process();
}

/// The loopback port every dead-daemon test points at (#4306, #4415).
///
/// Why this specific port, rather than one the fixture binds for itself: the
/// fixture needs an address that (a) refuses connections deterministically and
/// (b) cannot be taken over by any other binder. A privileged loopback port
/// satisfies both by construction, and — measured on this suite's two targets —
/// is the only shape that satisfies (a):
///
/// * **Refuses, immediately.** Nothing listens, so the kernel answers `RST` →
///   `ECONNREFUSED`. Measured on macOS: 30/30 connects refused, worst case
///   132µs. Contrast the obvious-looking alternative of binding a port and
///   deliberately NOT calling `listen(2)`: that reserves the port, but macOS
///   silently DROPS the `SYN` instead of resetting it, so the connect times out
///   (measured: `TimedOut` after a full 10s) rather than being refused — which
///   would convert every daemon-down test here into a slow one and hand the
///   error-classification tests a timeout where they assert on a connect error.
/// * **Unavailable to any other binder.** It is below 1024, so binding it needs
///   root / `CAP_NET_BIND_SERVICE` (measured: `PermissionDenied` for an
///   unprivileged bind of both `:1` and `:1023`), and it sits outside every
///   ephemeral range the OS allocates from (macOS 49152–65535, Linux
///   `ip_local_port_range` default 32768–60999), so it can never be handed out
///   as an ephemeral. Port 1 is IANA `tcpmux`, which has no modern
///   implementation.
///
/// What it replaces: three copies of a `dead_loopback_url()` helper
/// (`tui::project_ctl::poll::tests`, `tui::coordinator::tests`,
/// `tui::project_ctl::tests`) feeding 13 call sites, each of which obtained a
/// "known dead" address by binding an ephemeral port and then DROPPING the
/// listener. Dropping it returns the port to the OS ephemeral pool, so the
/// premise — "nothing can be listening here" — held only until some other
/// binder was handed that same port; then the connect SUCCEEDS and the test
/// asserts against a live socket (`classify_connect_error_is_transport`'s
/// `expect_err` panics, or the request fails at the HTTP layer and
/// misclassifies as `NonTransport`). #4415 observed exactly that failure.
/// Measured on macOS: a bind-and-drop port was reassigned to a competing
/// binder — and accepted a connection — after 16,103 ephemeral binds, i.e. one
/// wrap of the 49152–65535 range, which a full suite's port churn reaches.
const DEAD_LOOPBACK_PORT: u16 = 1;

/// Pin "privileged" at COMPILE time — it is a property of the constant, not of
/// any particular run, so a future edit that moves the port above 1024 (into
/// bindable, and eventually ephemeral, territory) must fail the build rather
/// than wait for a test to notice. Also what keeps
/// [`tests::dead_loopback_port_is_not_bindable_or_ephemeral`] free of a
/// `clippy::assertions_on_constants` lint.
const _: () = assert!(
    DEAD_LOOPBACK_PORT < 1024,
    "DEAD_LOOPBACK_PORT must stay below 1024 so binding it requires root and the \
     OS never hands it out as an ephemeral port (#4306/#4415)"
);

/// Verifies [`DEAD_LOOPBACK_PORT`]'s premise once per test process.
static DEAD_PORT_VERIFIED: Once = Once::new();

/// A `127.0.0.1` URL guaranteed to refuse every connection — the ONE
/// dead-address fixture for this crate (#4306, #4415).
///
/// Why: see [`DEAD_LOOPBACK_PORT`] for the full argument and the measurements.
/// What: returns `http://127.0.0.1:1`, having first confirmed (once per test
/// process) that the address really does refuse. That check turns the fixture's
/// one environmental assumption — "no process on this machine listens on
/// loopback port 1" — into an immediate, named failure instead of a confusing
/// downstream one: a `expect_err` panic or a `Transport`/`NonTransport`
/// misclassification several frames away, which is precisely the debugging
/// experience #4415 describes as training people to re-run rather than
/// investigate.
/// Test: [`tests::dead_loopback_url_refuses_every_connect`] and
/// [`tests::dead_loopback_port_is_not_bindable_or_ephemeral`] pin the two
/// properties, so the guarantee is verified rather than merely documented.
pub(crate) fn dead_loopback_url() -> String {
    DEAD_PORT_VERIFIED.call_once(|| {
        let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
        // A generous timeout: it bounds a pathological environment, it does not
        // define the expectation. The assertion is on the error KIND, so a
        // machine that refuses slowly still passes and one that accepts (or
        // silently drops) fails loudly, naming the remedy.
        match std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10)) {
            Ok(_) => panic!(
                "test_support::dead_loopback_url(): something is LISTENING on {addr}, so the \
                 dead-address fixture's premise is void (#4306/#4415). Stop that listener, or \
                 pick another privileged, non-ephemeral loopback port for DEAD_LOOPBACK_PORT."
            ),
            Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {}
            Err(e) => panic!(
                "test_support::dead_loopback_url(): {addr} neither accepted nor REFUSED the \
                 connect — it failed with {:?} ({e}) (#4306/#4415). The fixture requires a \
                 prompt ECONNREFUSED; a dropped SYN (TimedOut) would make every daemon-down \
                 test slow and would hand the error-classification tests the wrong error kind.",
                e.kind()
            ),
        }
    });
    format!("http://127.0.0.1:{DEAD_LOOPBACK_PORT}")
}

/// Prefix every hermetic test temp directory carries.
///
/// Why: `TempDir::drop` cannot run after a hard-killed test process (e.g. a
/// CI timeout `SIGKILL`), so some leakage is unavoidable. A stable, greppable
/// prefix makes any leaked directory trivially attributable to trusty-mpm's
/// test suite (vs. some other tool's temp files) and safe for
/// [`sweep_stale_test_dirs`] — or a human running `rm -rf` — to reap without
/// guessing.
pub(crate) const TEST_DIR_PREFIX: &str = "tm-test-";

/// An absolute, installed-looking `tm` path every hook-writing test can pin.
///
/// Why (#7244): the hooks writer refuses a build-artifact binary, and a test
/// process IS one. It then PATH-resolves an installed `tm`, which a developer
/// machine has and a CI runner does not — so every test that provisions hooks
/// asserted one thing locally and aborted on a refusal in CI. Pinning a path
/// that passes both of the writer's gates (absolute, not a build tree, a stem
/// this crate ships) makes those tests hermetic: they assert the WRITE, which
/// is what they are about, on any host.
/// What: a literal path, never created and never executed — the resolver only
/// inspects the spelling. It must stay outside any temp root, since
/// [`trusty_common::bin_resolve::is_ephemeral_build_path`] refuses those too,
/// which rules out a `TempDir`-hosted fake.
/// Test: every caller; the refusal arms themselves are covered by
/// `resolve_stable_hook_exe_with_refuses_an_ephemeral_exe` and siblings.
pub(crate) const STABLE_HOOK_EXE: &str = "/usr/local/bin/tm";

/// How long a `tm-test-*` directory may sit in the hermetic root before
/// [`sweep_stale_test_dirs`] treats it as leaked and removes it.
const STALE_AFTER: Duration = Duration::from_secs(24 * 60 * 60);

static SWEEP_ONCE: Once = Once::new();

/// Return the real, hardcoded OS temp root.
///
/// Why: deliberately NOT `std::env::temp_dir()`, which honors `$TMPDIR` —
/// the exact mechanism #3382's incident exploited. The chosen rule: on Unix
/// (macOS + Linux, the only targets trusty-mpm's test suite runs on — CI is
/// `ubuntu-latest`, local dev is macOS), always resolve to `/tmp`. It is the
/// one system scratch path that exists on every Unix trusty-mpm supports,
/// is never inside a user's home or project tree, and is NOT configurable
/// via any environment variable — so no inherited `TMPDIR`, however
/// polluted, can ever redirect it. On non-Unix targets (the Tauri GUI's
/// Windows builds only; no test suite runs there) this falls back to
/// `std::env::temp_dir()`, since Windows has no established TMPDIR-pollution
/// pattern and no equivalent always-present hardcoded system path.
/// What: returns `/tmp` on Unix, `std::env::temp_dir()` otherwise.
/// Test: [`tests::real_system_tmp_ignores_tmpdir_env`].
fn real_system_tmp() -> PathBuf {
    #[cfg(unix)]
    {
        PathBuf::from("/tmp")
    }
    #[cfg(not(unix))]
    {
        std::env::temp_dir()
    }
}

/// Whether `home` is a "meaningful" (non-degenerate) containment boundary
/// relative to `candidate` — more than one path component, and not
/// identical to `candidate` itself.
///
/// Why: shared by [`guard_against_project_tree`] AND its own test suite
/// (`tests::hermetic_temp_dir_is_prefixed_and_outside_home`) so both apply
/// the EXACT same degenerate-`$HOME` exclusion — a code reviewer reproduced
/// a real crash by running the compiled test binary with `HOME=/tmp` (or
/// `HOME=/`): every real system temp path trivially "starts with" `/`, and
/// `/tmp` trivially equals `HOME=/tmp`, so a naive `Path::starts_with` check
/// panicked on EVERY [`hermetic_temp_dir`] call in that environment —
/// strictly worse than the pre-fix behavior, not defense in depth. Having
/// the assertion logic live in one place prevents the test suite's own
/// containment check from drifting out of sync with the production check and
/// reintroducing the same false positive from the other direction.
/// What: `home.components().count() > 1 && home != candidate`.
fn is_meaningful_home_boundary(home: &Path, candidate: &Path) -> bool {
    home.components().count() > 1 && home != candidate
}

/// Panic loudly if `root` resolves inside the user's home directory.
///
/// Why: defense in depth. [`real_system_tmp`]'s hardcoded `/tmp` never trips
/// this in practice, but if that function is ever changed to consult an env
/// var again (or a future platform's fallback resolves somewhere
/// unexpected), a hermetic root that lands inside `$HOME` — where every
/// observed project tree lives — must fail the test run loudly rather than
/// silently littering it, per #3382.
/// What: compares `root` against `$HOME` with `Path::starts_with`, but ONLY
/// when [`is_meaningful_home_boundary`] says `$HOME` is non-degenerate
/// relative to `root`. No-ops if `$HOME` is unset.
/// Test: [`tests::guard_panics_on_genuine_home_containment`],
/// [`tests::guard_does_not_panic_when_home_is_tmp`],
/// [`tests::guard_does_not_panic_when_home_is_root`].
fn guard_against_project_tree(root: &Path) {
    let Some(home) = std::env::var_os("HOME") else {
        return;
    };
    let home = PathBuf::from(home);
    if is_meaningful_home_boundary(&home, root) && root.starts_with(&home) {
        panic!(
            "hermetic test temp root {root:?} resolves inside $HOME ({home:?}) — \
             refusing to risk littering a project tree (see #3382)"
        );
    }
}

/// Create a hermetic test `TempDir`, immune to inherited `$TMPDIR` pollution.
///
/// Why: the one replacement for every bare `TempDir::new()` call site
/// flagged by #3382 — rooting under [`real_system_tmp`] instead of
/// `env::temp_dir()` means a polluted `TMPDIR` can never again cause test
/// scaffolding to land in a user's project tree.
/// What: sweeps stale leaked directories once per test process (see
/// [`sweep_stale_test_dirs`]), then creates a `TempDir` under the hermetic
/// root with the [`TEST_DIR_PREFIX`] prefix.
/// Test: [`tests::hermetic_temp_dir_is_prefixed_and_outside_home`].
pub(crate) fn hermetic_temp_dir() -> TempDir {
    SWEEP_ONCE.call_once(sweep_stale_test_dirs);
    let root = real_system_tmp();
    guard_against_project_tree(&root);
    tempfile::Builder::new()
        .prefix(TEST_DIR_PREFIX)
        .tempdir_in(&root)
        .expect("create hermetic test temp dir")
}

/// Best-effort sweep of stale `tm-test-*` directories left behind by
/// hard-killed test processes.
///
/// Why: `TempDir::drop` cannot fire after a `SIGKILL`, so leaked directories
/// are an accepted possibility, not a bug to eliminate outright — #3382's
/// incident was 50 of them accumulating over ~24h with nothing reaping them.
/// A lightweight sweep at the start of a test process bounds that growth
/// without a background daemon, proportionate to how rare and small the
/// leaks actually are.
/// What: reads [`real_system_tmp`], and for every entry whose name starts
/// with [`TEST_DIR_PREFIX`] and whose modified time is more than
/// [`STALE_AFTER`] old, best-effort `remove_dir_all`s it. All errors
/// (permission, concurrent removal by another test process racing the same
/// sweep, a non-existent root) are silently ignored — this is hygiene, not a
/// correctness requirement, so it must never fail a test run.
/// Test: [`tests::sweep_removes_only_stale_prefixed_dirs`].
fn sweep_stale_test_dirs() {
    let root = real_system_tmp();
    let Ok(entries) = std::fs::read_dir(&root) else {
        return;
    };
    let now = SystemTime::now();
    for entry in entries.flatten() {
        let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
            continue;
        };
        if !name.starts_with(TEST_DIR_PREFIX) {
            continue;
        }
        let is_stale = entry
            .metadata()
            .and_then(|meta| meta.modified())
            .ok()
            .and_then(|modified| now.duration_since(modified).ok())
            .is_some_and(|age| age > STALE_AFTER);
        if is_stale {
            let _ = std::fs::remove_dir_all(entry.path());
        }
    }
}

/// Point every trusty-* daemon lookup at a scratch data directory for the body
/// of one `#[serial_test::serial]` test, and restore the environment on drop.
///
/// Why here: two suites need it — `session_manager::search_gc_guard_tests` and
/// `session_manager::index_delete_guard::tests` — and both are asserting that a
/// test process does NOT reach the operator's real trusty-search daemon (#4743).
/// A per-suite copy of a guard whose whole job is to keep production state out
/// of a test run is the wrong thing to have two of.
///
/// Why a guard rather than the set / call / `remove_var` sequence written inline
/// elsewhere in this crate: a panicking assertion between the set and the remove
/// leaks process-global state into every later test in the binary. `Drop` runs
/// on the unwind.
///
/// Callers MUST be tagged `#[serial_test::serial]` — the variables are
/// process-global, so two tests holding one of these at once see each other's
/// values.
/// Test: `session_manager::index_delete_guard::tests::acquire_refuses_when_no_daemon_socket_is_bound`
/// and the other users listed above.
pub(crate) struct DaemonHomeOverride {
    allow_production: bool,
}

impl DaemonHomeOverride {
    /// Resolve daemon data under `data_dir`. With `allow_production`, also lift
    /// the #4743 refusal that stops a test process destroying real index data.
    pub(crate) fn new(data_dir: &Path, allow_production: bool) -> Self {
        // SAFETY: callers are `#[serial]` — no other test thread races this.
        unsafe {
            std::env::set_var(trusty_common::DATA_DIR_OVERRIDE_ENV, data_dir);
            if allow_production {
                std::env::set_var(trusty_common::test_harness::ALLOW_PRODUCTION_ENV, "1");
            }
        }
        Self { allow_production }
    }
}

impl Drop for DaemonHomeOverride {
    fn drop(&mut self) {
        // SAFETY: `#[serial]` — see `DaemonHomeOverride::new`.
        unsafe {
            std::env::remove_var(trusty_common::DATA_DIR_OVERRIDE_ENV);
            if self.allow_production {
                std::env::remove_var(trusty_common::test_harness::ALLOW_PRODUCTION_ENV);
            }
        }
    }
}

/// An isolated daemon data directory plus the override pointing resolution at
/// it. Both must stay alive for the test's duration.
pub(crate) fn isolated_daemon_home(allow_production: bool) -> (TempDir, DaemonHomeOverride) {
    let dir = hermetic_temp_dir();
    let override_guard = DaemonHomeOverride::new(dir.path(), allow_production);
    (dir, override_guard)
}

/// A fake, executable `claude` first on `PATH`, plus the guard that restores
/// `PATH` when the test ends (#7862).
///
/// Why: every route that reaches `ClaudeCodeAdapter::spawn_resume` resolves the
/// `claude` binary before it types anything into the pane, so a test driving
/// that route on a machine with no Claude Code install stops at the adapter and
/// observes nothing past it — green on a developer laptop, red on every CI
/// runner. Winning the lookup outright also stops a machine that DOES have
/// `claude` from launching the operator's real one.
/// What: writes `#!/bin/sh` + `exit 0` at `<tempdir>/claude`, mode 0755, and
/// prepends that directory to `PATH` — `bin_resolve::resolve_binary` consults
/// the live `PATH` before its well-known-dirs fallback. Both the directory and
/// the previous `PATH` live in the returned guard, so the stub survives exactly
/// as long as the test and `Drop` restores the environment on the unwind too.
///
/// Callers MUST be tagged `#[serial_test::serial]`: `PATH` is process-global.
/// Test: `daemon::managed_routes::resume_claim_tests::the_claim_is_still_held_when_the_route_types_into_the_pane`.
#[cfg(unix)]
pub(crate) fn fake_claude_on_path() -> FakeClaudeOnPath {
    use std::os::unix::fs::PermissionsExt;
    let dir = hermetic_temp_dir();
    let exe = dir.path().join("claude");
    std::fs::write(&exe, b"#!/bin/sh\nexit 0\n").expect("write the fake claude");
    std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755))
        .expect("chmod the fake claude");
    let prev = std::env::var_os("PATH");
    let mut entries = vec![dir.path().to_path_buf()];
    if let Some(ref p) = prev {
        entries.extend(std::env::split_paths(p));
    }
    let joined = std::env::join_paths(entries).expect("join PATH");
    // SAFETY: callers are `#[serial]`, and `Drop` restores the previous value.
    unsafe { std::env::set_var("PATH", joined) };
    assert!(
        trusty_common::bin_resolve::resolve_binary("claude").is_some_and(|found| found == exe),
        "the planted stub must WIN the lookup, else the test it guards still \
         depends on the host's own Claude Code install"
    );
    FakeClaudeOnPath { _dir: dir, prev }
}

/// The live half of [`fake_claude_on_path`] — see its doc.
#[cfg(unix)]
pub(crate) struct FakeClaudeOnPath {
    /// Holds the stub's directory alive for the guard's lifetime.
    _dir: TempDir,
    prev: Option<std::ffi::OsString>,
}

#[cfg(unix)]
impl Drop for FakeClaudeOnPath {
    fn drop(&mut self) {
        // SAFETY: as in `fake_claude_on_path`.
        unsafe {
            match self.prev.take() {
                Some(p) => std::env::set_var("PATH", p),
                None => std::env::remove_var("PATH"),
            }
        }
    }
}

/// Make `tracing` events reachable by a thread-local capturing subscriber, for
/// the whole test process (#4931).
///
/// Why: `tracing`'s macros short-circuit on a process-global `MAX_LEVEL` that
/// starts at `OFF` and is raised only when some subscriber is installed as the
/// GLOBAL default. `tracing::subscriber::with_default` installs a THREAD-LOCAL
/// one, which never raises it — so whether a capture test recorded anything
/// depended on whether some unrelated test in the same binary had happened to
/// install a global default first. `ensure_managed_config_dir_emits_the_frozen_skill_warning`
/// failed that way at a measured 6-7 runs in 20, always with `Captured lines: []`,
/// and `#[serial]` cannot help: the level is global and outlives the lock.
///
/// This is the ONE place that raises it. Two test modules had grown their own
/// copy of the `Once` + `set_global_default` block; a third would be free to
/// install a FILTERED global instead, which would clamp `MAX_LEVEL` below `WARN`
/// and silently reintroduce the flake for every capture test in the binary. The
/// post-condition below turns that into an immediate, named failure rather than
/// an empty capture several frames away.
/// What: installs a bare `tracing_subscriber::registry()` as the global default
/// once per process (its `max_level_hint` is `None`, i.e. `TRACE`), then asserts
/// the resulting global level admits `WARN`. A thread-local `with_default` still
/// overrides the global for the capturing thread.
/// Test: [`tests::event_capture_admits_warn`]; used by
/// `core::managed_config_tests::ensure_managed_config_dir_emits_the_frozen_skill_warning`
/// and `session_manager::dedup_tests`.
pub(crate) fn enable_event_capture() {
    static RAISE_MAX_LEVEL: Once = Once::new();
    RAISE_MAX_LEVEL.call_once(|| {
        // A global default may already be installed; that is fine as long as it
        // does not clamp the level, which the assertion below verifies.
        let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
    });
    assert!(
        tracing::level_filters::LevelFilter::current() >= tracing::Level::WARN,
        "test_support::enable_event_capture(): the process-global tracing level \
         is {:?}, which discards WARN events before any subscriber sees them \
         (#4931). Some test in this binary installed a FILTERED global default \
         subscriber; route it through this function, or give it a subscriber \
         whose max_level_hint admits WARN.",
        tracing::level_filters::LevelFilter::current()
    );
}

/// The one tmux server a self-describing fake pane reports (#9101).
pub(crate) const FAKE_PANE_SERVER: &str = "1:1";

/// A fake tmux pane id that names its session: `%<name>` (#9101).
///
/// Why: a fake driver must answer `pane_identity` with the session that holds
/// the pane, or `same_server` refuses every pane operation. A pane id that
/// carries its session name lets the fake compute that from the id alone.
/// Test: `a_self_describing_pane_names_its_own_session`.
pub(crate) fn self_describing_pane(name: &str) -> String {
    format!("%{name}")
}

/// The identity of a [`self_describing_pane`]: its session is the one the id
/// names, on [`FAKE_PANE_SERVER`] (#9101).
/// Test: `a_self_describing_pane_names_its_own_session`.
pub(crate) fn self_describing_identity(
    pane_id: &str,
) -> crate::session_manager::pane_identity::PaneIdentity {
    crate::session_manager::pane_identity::PaneIdentity {
        pane_id: pane_id.to_owned(),
        session_id: "$0".into(),
        server: FAKE_PANE_SERVER.into(),
        session_name: pane_id.trim_start_matches('%').to_owned(),
    }
}

#[cfg(test)]
mod tests {
    /// #9101: the identity the fake derives names the session the pane id
    /// was minted for, on the one fake server.
    #[test]
    fn a_self_describing_pane_names_its_own_session() {
        let pane = super::self_describing_pane("tmpm-test-9101");
        let identity = super::self_describing_identity(&pane);
        assert_eq!(identity.pane_id, "%tmpm-test-9101");
        assert_eq!(identity.session_name, "tmpm-test-9101");
        assert_eq!(identity.server, super::FAKE_PANE_SERVER);
    }

    use super::*;
    use std::time::UNIX_EPOCH;

    /// #8545: the constructor ran and fences the operator's real home. Reads
    /// the password-database home, which no sibling test repoints.
    #[test]
    fn the_home_write_fence_is_armed_for_the_lib_binary() {
        use crate::core::home_write_fence::{armed_roots, fenced_root};
        let home = crate::core::host_state_gate::passwd_home_dir().expect("a passwd home");
        for dest in [
            home.join(".trusty-mpm").join("usage"),
            home.join(".claude").join("settings.json"),
            home.join(".claude.json"),
        ] {
            assert!(
                fenced_root(&dest, armed_roots()).is_some(),
                "{} is not fenced; armed roots: {:?}",
                dest.display(),
                armed_roots()
            );
        }
    }

    /// `real_system_tmp` must ignore `$TMPDIR` even when it points somewhere
    /// that would otherwise cause litter (e.g. a project tree).
    #[test]
    fn real_system_tmp_ignores_tmpdir_env() {
        // Safety/portability note: this test only asserts the *return value*
        // is independent of TMPDIR; it does not mutate global env state.
        let root = real_system_tmp();
        #[cfg(unix)]
        assert_eq!(root, PathBuf::from("/tmp"));
        assert!(root.exists(), "hermetic root must exist: {root:?}");
    }

    /// Why serial: reads `$HOME` (via `guard_against_project_tree`, indirectly
    /// through `hermetic_temp_dir`) and asserts on it below. Must be
    /// serialized against every other test in this binary that mutates or
    /// reads `$HOME` for the same reason — same shared default group as
    /// `session_manager::workspace_guard::tests::is_safe_to_remove_rejects_home`
    /// (#2461 sweep) and the three `HomeOverride`-mutating tests below; a
    /// code reviewer noted the mutating tests' own `Mutex` only serializes
    /// them against each other, not against `$HOME`-reading tests elsewhere,
    /// which `#[serial]`'s shared default group closes.
    #[serial_test::serial]
    #[test]
    fn hermetic_temp_dir_is_prefixed_and_outside_home() {
        let dir = hermetic_temp_dir();
        let name = dir
            .path()
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or_default();
        assert!(
            name.starts_with(TEST_DIR_PREFIX),
            "expected {name:?} to start with {TEST_DIR_PREFIX:?}"
        );
        // Same degenerate-HOME exclusion as `guard_against_project_tree`
        // (compared against the ROOT, not the leaf `dir.path()` — otherwise
        // this assertion reintroduces the exact false positive it's meant to
        // catch: `dir.path()` is always a child of, never equal to, the
        // root, so a naive `home != dir.path()` check is never degenerate).
        if let Some(home) = std::env::var_os("HOME") {
            let home = PathBuf::from(home);
            if is_meaningful_home_boundary(&home, &real_system_tmp()) {
                assert!(
                    !dir.path().starts_with(&home),
                    "hermetic dir must never resolve inside $HOME: {:?}",
                    dir.path()
                );
            }
        }
    }

    /// RAII guard that overrides `$HOME` for the duration of a `#[serial]`
    /// test and restores the prior value on drop (including on panic-driven
    /// unwind, via the `#[should_panic]` test below) — mirrors
    /// `core::session_launch::tests::EnvVarGuard`'s established pattern in
    /// this crate.
    ///
    /// Why NOT an internal `Mutex` (a code reviewer flagged an earlier
    /// revision that had one): a `Mutex` scoped to this struct only
    /// serializes `HomeOverride`-using tests against EACH OTHER, not against
    /// every other test in this binary that reads `$HOME` without going
    /// through this guard — e.g.
    /// [`hermetic_temp_dir_is_prefixed_and_outside_home`] above, or
    /// `session_manager::workspace_guard::tests::is_safe_to_remove_rejects_home`.
    /// `#[serial_test::serial]`'s shared default group, tagged on every one
    /// of those tests, closes that gap crate-wide instead of only locally.
    struct HomeOverride {
        prev: Option<std::ffi::OsString>,
    }

    impl Drop for HomeOverride {
        fn drop(&mut self) {
            // SAFETY: every caller of `override_home` is `#[serial]`, so no
            // other test thread races this set/restore.
            match self.prev.take() {
                Some(v) => unsafe { std::env::set_var("HOME", v) },
                None => unsafe { std::env::remove_var("HOME") },
            }
        }
    }

    /// Set `$HOME` to `value`, returning a guard that restores it on drop.
    /// Callers MUST be tagged `#[serial_test::serial]` — see [`HomeOverride`].
    fn override_home(value: &str) -> HomeOverride {
        let prev = std::env::var_os("HOME");
        // SAFETY: caller is `#[serial]`.
        unsafe { std::env::set_var("HOME", value) };
        HomeOverride { prev }
    }

    /// The genuine positive case: a real per-user home (e.g. `/Users/x`,
    /// `/home/x`) containing the candidate root must still panic.
    ///
    /// Why serial: mutates `$HOME` via [`override_home`] — see
    /// [`HomeOverride`] for why serialization must be crate-wide, not a
    /// locally scoped lock.
    #[serial_test::serial]
    #[test]
    #[should_panic(expected = "resolves inside $HOME")]
    fn guard_panics_on_genuine_home_containment() {
        let _home = override_home("/Users/test-user");
        guard_against_project_tree(&PathBuf::from("/Users/test-user/trusty-mpm-projects"));
    }

    /// Regression for the false-positive a code reviewer reproduced: with
    /// `HOME=/tmp` (root-container / OpenShift-style environments),
    /// `real_system_tmp()`'s `/tmp` trivially equals `$HOME`, so a naive
    /// `starts_with` check panicked on EVERY `hermetic_temp_dir()` call —
    /// strictly worse than the pre-fix behavior. Must be a no-op.
    ///
    /// Why serial: see [`guard_panics_on_genuine_home_containment`].
    #[serial_test::serial]
    #[test]
    fn guard_does_not_panic_when_home_is_tmp() {
        let _home = override_home("/tmp");
        guard_against_project_tree(&PathBuf::from("/tmp"));
    }

    /// Regression for the same false-positive with `HOME=/`: every absolute
    /// path trivially "starts with" `/`, so a naive check panicked on every
    /// `hermetic_temp_dir()` call. Must be a no-op.
    ///
    /// Why serial: see [`guard_panics_on_genuine_home_containment`].
    #[serial_test::serial]
    #[test]
    fn guard_does_not_panic_when_home_is_root() {
        let _home = override_home("/");
        guard_against_project_tree(&PathBuf::from("/tmp"));
    }

    #[test]
    fn sweep_removes_only_stale_prefixed_dirs() {
        let root = real_system_tmp();
        let stale = root.join(format!(
            "{TEST_DIR_PREFIX}sweep-stale-{}",
            std::process::id()
        ));
        let fresh = root.join(format!(
            "{TEST_DIR_PREFIX}sweep-fresh-{}",
            std::process::id()
        ));
        let unrelated = root.join(format!("not-tm-prefixed-{}", std::process::id()));
        std::fs::create_dir_all(&stale).unwrap();
        std::fs::create_dir_all(&fresh).unwrap();
        std::fs::create_dir_all(&unrelated).unwrap();

        // Back-date the "stale" directory's mtime by 2 days (std::fs::FileTimes,
        // stable since 1.75 — no need for an extra crate dependency here).
        let two_days_ago = SystemTime::now() - Duration::from_secs(2 * 24 * 60 * 60);
        let times = std::fs::FileTimes::new().set_modified(two_days_ago);
        std::fs::File::open(&stale)
            .unwrap()
            .set_times(times)
            .unwrap();

        sweep_stale_test_dirs();

        assert!(!stale.exists(), "stale tm-test- dir should be swept");
        assert!(fresh.exists(), "fresh tm-test- dir should survive");
        assert!(unrelated.exists(), "non-prefixed dir must never be touched");

        // Clean up what the sweep left behind (this test writes directly
        // under the real /tmp, not a TempDir, so nothing auto-removes them).
        let _ = std::fs::remove_dir_all(&fresh);
        let _ = std::fs::remove_dir_all(&unrelated);
        let _ = std::fs::remove_dir_all(&stale);
    }

    /// #4306/#4415 property 1 of 2: the address must answer `ECONNREFUSED`,
    /// and must do so on EVERY attempt.
    ///
    /// Deliberately asserts the error KIND, not merely that the connect failed.
    /// That distinction is load-bearing and was not academic: the first attempt
    /// at this fixture reserved a port by binding it without `listen(2)`, which
    /// looked correct and which a "did it fail?" assertion would have passed —
    /// macOS silently drops the `SYN` there, so connects TIME OUT rather than
    /// being refused. `classify_connect_error_is_transport` would still have
    /// gone green (reqwest maps both to `Transport`) while every daemon-down
    /// test quietly became a multi-second one. Asserting the kind is what
    /// caught it.
    #[test]
    fn dead_loopback_url_refuses_every_connect() {
        let addr: std::net::SocketAddr = dead_loopback_url()
            .trim_start_matches("http://")
            .parse()
            .expect("the fixture's url must be host:port after the scheme");

        for attempt in 0..50 {
            let err = std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10))
                .expect_err("nothing may be listening on the dead-address port");
            assert_eq!(
                err.kind(),
                std::io::ErrorKind::ConnectionRefused,
                "attempt {attempt} to {addr} must be REFUSED (RST), not dropped: {err:?}"
            );
        }
    }

    /// #4306/#4415 property 2 of 2: the port must be unavailable to any other
    /// binder. This is the half the old bind-and-drop fixture lacked, and the
    /// direct cause of the observed flake — so it is asserted, not assumed.
    ///
    /// Two independent guarantees, checked separately because either one alone
    /// would be enough to break if a future edit moved `DEAD_LOOPBACK_PORT`
    /// into the ephemeral range or above 1024.
    #[test]
    fn dead_loopback_port_is_not_bindable_or_ephemeral() {
        // (a) Privileged: an unprivileged bind must be refused outright, so no
        //     test — nor any other unprivileged process — can start listening.
        // "privileged" itself is pinned at compile time next to the constant.
        let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
        assert!(
            std::net::TcpListener::bind(addr).is_err(),
            "an unprivileged bind of {addr} must fail — a bindable dead-address port is \
             exactly the invalidated premise #4306 describes"
        );

        // (b) Never ephemeral: the OS must not hand this port to a competing
        //     binder. 2,000 binds is a cheap sanity sweep rather than a proof
        //     (the proof is that the port is below every ephemeral range); it
        //     exists to catch a future edit that moves the constant.
        for _ in 0..2_000 {
            if let Ok(other) = std::net::TcpListener::bind("127.0.0.1:0") {
                assert_ne!(
                    other.local_addr().expect("local addr").port(),
                    DEAD_LOOPBACK_PORT,
                    "the OS handed a competing binder the dead-address port"
                );
            }
        }
    }

    /// Guards against `UNIX_EPOCH` underflow bugs in age math (belt-and-braces).
    #[test]
    fn stale_after_is_positive_duration() {
        assert!(STALE_AFTER > Duration::from_secs(0));
        assert!(SystemTime::now().duration_since(UNIX_EPOCH).is_ok());
    }

    /// #4931: the whole point of [`super::enable_event_capture`] is that a
    /// `tracing::warn!` reaches a thread-local subscriber afterwards. Pin both
    /// halves — the global level admits `WARN`, and a `with_default` capture
    /// installed after the call actually records one.
    #[test]
    fn event_capture_admits_warn() {
        use tracing_subscriber::layer::SubscriberExt;

        super::enable_event_capture();
        assert!(
            tracing::level_filters::LevelFilter::current() >= tracing::Level::WARN,
            "the global level must admit WARN after enable_event_capture()"
        );

        let buffer = trusty_common::log_buffer::LogBuffer::new(8);
        let subscriber = tracing_subscriber::registry().with(
            trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
        );
        tracing::subscriber::with_default(subscriber, || {
            tracing::warn!("enable-event-capture-probe");
        });

        let lines = buffer.tail(8);
        assert!(
            lines
                .iter()
                .any(|l| l.contains("enable-event-capture-probe")),
            "a WARN emitted under with_default must reach the capture buffer,              else every capture test in this binary is silently vacuous: {lines:#?}"
        );
    }
}