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
//! 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 ;
use Once;
use ;
use 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`.
pub
/// 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 use cratedisclaimed_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 use crateenv_test_lock as lock_path_env;
/// 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!;
/// Verifies [`DEAD_LOOPBACK_PORT`]'s premise once per test process.
static DEAD_PORT_VERIFIED: 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
/// 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 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 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 = from_secs;
static SWEEP_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`].
/// 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`.
/// 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`].
/// 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
/// 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`].
/// 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
/// An isolated daemon data directory plus the override pointing resolution at
/// it. Both must stay alive for the test's duration.
pub
/// 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`.
pub
/// The live half of [`fake_claude_on_path`] — see its doc.
pub
/// 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