trusty-common 0.51.0

Shared utilities and provider-agnostic streaming chat (ChatProvider, OllamaProvider, OpenRouter, tool-use) for trusty-* projects
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
//! Robust executable discovery and daemon `PATH` composition.
//!
//! Why: macOS launchd relaunches LaunchAgents with a deliberately minimal
//! `PATH` (`/usr/bin:/bin:/usr/sbin:/sbin`). Daemons that shell out to tools
//! installed by Homebrew (`/opt/homebrew/bin`, `/usr/local/bin`) or into the
//! user's home (`~/.local/bin`, `~/.cargo/bin`) therefore fail *before*
//! reaching application logic — e.g. the trusty-mpm session manager could not
//! find `tmux` or `claude` after every daemon restart (#1298). Two daemons
//! independently hand-rolled `which`-style lookups and would each have to
//! re-derive the same well-known-dir list; this module is the single shared
//! answer.
//!
//! What: [`daemon_path_dirs`] returns the ordered, de-duplicated list of bin
//! directories a trusty-* daemon should be able to see (Homebrew + user bins
//! before the standard system dirs, with `~` expanded to the real home).
//! [`daemon_path_env`] joins them into a `PATH` string suitable for a launchd
//! `EnvironmentVariables` dict. [`resolve_binary`] finds an executable by
//! consulting the live `PATH` first and falling back to those well-known dirs,
//! so a daemon spawned with a minimal `PATH` still locates `tmux`/`claude`.
//!
//! Test: `daemon_path_*` and `resolve_binary_*` unit tests below. The module is
//! cross-platform (the well-known dirs are macOS/Linux-oriented but harmless
//! elsewhere) so it is not gated behind `#[cfg(target_os = "macos")]`.
//!
//! [`daemon_path_dirs`]: crate::bin_resolve::daemon_path_dirs
//! [`daemon_path_env`]: crate::bin_resolve::daemon_path_env
//! [`resolve_binary`]: crate::bin_resolve::resolve_binary

use std::path::{Path, PathBuf};

/// Standard system bin directories present even under launchd's minimal `PATH`.
///
/// Why: these must always be in the composed `PATH` so core utilities
/// (`/bin/sh`, `/usr/bin/env`, …) resolve. They go *after* the user/Homebrew
/// dirs so a Homebrew tool shadows an older system copy when both exist.
const SYSTEM_BIN_DIRS: &[&str] = &["/usr/bin", "/bin", "/usr/sbin", "/sbin"];

/// Absolute (non-home) bin directories that hold operator-installed tools.
///
/// Why: Homebrew installs to `/opt/homebrew/bin` (Apple silicon) or
/// `/usr/local/bin` (Intel); both must be visible to the daemon. Listed before
/// the home-relative dirs only for readability — final ordering is
/// user/Homebrew first, then system, enforced in [`daemon_path_dirs`].
const ABSOLUTE_TOOL_DIRS: &[&str] = &["/opt/homebrew/bin", "/usr/local/bin"];

/// Home-relative bin directories (expanded against the real home dir).
///
/// Why: `claude` ships to `~/.local/bin` and cargo-installed binaries land in
/// `~/.cargo/bin`; launchd never expands `~`, so the daemon must carry the
/// expanded absolute paths.
const HOME_RELATIVE_BIN_DIRS: &[&str] = &[".local/bin", ".cargo/bin"];

/// Compose the ordered, de-duplicated list of bin directories a trusty-*
/// daemon should be able to see, with `~` expanded to the real home.
///
/// Why: launchd's minimal `PATH` omits Homebrew and user bin dirs, breaking
/// daemon spawns of `tmux`/`claude` (#1298). A single canonical ordering keeps
/// the generated plist `PATH` and the runtime [`resolve_binary`] fallback in
/// agreement.
/// What: returns Homebrew/absolute tool dirs, then the home-relative dirs
/// (`~/.local/bin`, `~/.cargo/bin`) expanded against [`dirs::home_dir`], then
/// the standard system dirs — de-duplicated, preserving first-seen order.
/// Existing entries from the live `PATH` are intentionally *not* merged here;
/// callers that want the inherited `PATH` too should prepend it.
/// Test: `daemon_path_dirs_orders_user_before_system`,
/// `daemon_path_dirs_expands_home`, `daemon_path_dirs_dedupes`.
pub fn daemon_path_dirs() -> Vec<PathBuf> {
    let mut dirs: Vec<PathBuf> = Vec::new();
    let push = |p: PathBuf, acc: &mut Vec<PathBuf>| {
        if !acc.contains(&p) {
            acc.push(p);
        }
    };

    for d in ABSOLUTE_TOOL_DIRS {
        push(PathBuf::from(d), &mut dirs);
    }
    if let Some(home) = dirs::home_dir() {
        for rel in HOME_RELATIVE_BIN_DIRS {
            push(home.join(rel), &mut dirs);
        }
    }
    for d in SYSTEM_BIN_DIRS {
        push(PathBuf::from(d), &mut dirs);
    }
    dirs
}

/// Render [`daemon_path_dirs`] as a colon-joined `PATH` string.
///
/// Why: a launchd `EnvironmentVariables` dict needs `PATH` as a single string;
/// generating it from the same source as the runtime fallback guarantees the
/// installed daemon and the live resolver look in identical places.
/// What: joins [`daemon_path_dirs`] with `:`, skipping any path that is not
/// valid UTF-8 (launchd plist values are UTF-8 strings).
/// Test: `daemon_path_env_contains_expected_dirs`.
pub fn daemon_path_env() -> String {
    daemon_path_dirs()
        .into_iter()
        .filter_map(|p| p.to_str().map(str::to_owned))
        .collect::<Vec<_>>()
        .join(":")
}

/// Resolve an executable by name, trusting the live `PATH` first and falling
/// back to the well-known [`daemon_path_dirs`].
///
/// Why: a daemon relaunched by launchd with a minimal `PATH` cannot find
/// `tmux`/`claude` via a bare `PATH` lookup, yet the binaries exist at known
/// locations. Checking those locations after the `PATH` lookup makes spawning
/// resilient to the inherited environment without trusting it.
/// What: if `name` contains a path separator it is treated as a literal path
/// and returned when it is an existing file. Otherwise each entry of the
/// current process `PATH` is checked, then each [`daemon_path_dirs`] entry, for
/// an existing `dir/name`; the first hit is returned. Returns `None` if nothing
/// matches.
/// Test: `resolve_binary_finds_in_well_known_dir`,
/// `resolve_binary_finds_a_binary_outside_the_process_path`,
/// `resolve_binary_returns_none_for_missing`,
/// `resolve_binary_accepts_absolute_path`.
pub fn resolve_binary(name: &str) -> Option<PathBuf> {
    resolve_binary_in(name, &daemon_path_dirs())
}

/// [`resolve_binary`] with the fallback directory list supplied by the caller.
///
/// Why (#4125): the fallback half of [`resolve_binary`] — the half that makes a
/// launchd-spawned daemon able to find a Homebrew binary its minimal `PATH`
/// omits — had no test that actually exercised it. The existing
/// `resolve_binary_finds_in_well_known_dir` deliberately avoids mutating the
/// process-global `PATH` (that races the parallel harness), so it could only
/// test [`candidate`] and the explicit-path branch; the real
/// "found somewhere the process `PATH` does not list" behaviour went unproven,
/// which is exactly the behaviour `locate_uv` was missing when it hard-failed
/// the py-embedder bootstrap on a daemon whose `PATH` lacked
/// `/opt/homebrew/bin`. Parameterizing the fallback list makes that branch
/// directly testable against a temp dir with no `PATH` mutation at all.
/// What: identical to [`resolve_binary`] except `fallback_dirs` replaces
/// [`daemon_path_dirs`] as step 2's search list.
/// Test: `resolve_binary_finds_a_binary_outside_the_process_path`.
fn resolve_binary_in(name: &str, fallback_dirs: &[PathBuf]) -> Option<PathBuf> {
    // An explicit path (absolute or relative with a separator) is used verbatim.
    if name.contains(std::path::MAIN_SEPARATOR) {
        let p = PathBuf::from(name);
        return p.is_file().then_some(p);
    }

    // 1) Honour the live PATH (covers interactive/login invocations).
    if let Some(path_var) = std::env::var_os("PATH") {
        for dir in std::env::split_paths(&path_var) {
            if let Some(hit) = candidate(&dir, name) {
                return Some(hit);
            }
        }
    }

    // 2) Fall back to the well-known daemon dirs (covers launchd's minimal PATH).
    for dir in fallback_dirs {
        if let Some(hit) = candidate(dir, name) {
            return Some(hit);
        }
    }
    None
}

/// Return `dir/name` when it is an existing, runnable file, else `None`.
///
/// Why: factoring the join+exists check keeps [`resolve_binary`] readable and
/// the "is this a runnable file" predicate in one place. A bare `is_file` check
/// is too loose: a non-executable regular file (e.g. a stray `claude.json` or a
/// data file that happens to share a name) would be returned and then fail at
/// spawn time. Requiring the execute bit on Unix means resolution only yields
/// paths the daemon can actually `exec`.
/// What: joins `dir` and `name`. On Unix the result is returned only when it is
/// a file *and* at least one execute bit (`0o111`) is set in its permissions; a
/// symlink to an executable file also satisfies this (metadata follows the
/// link). On non-Unix targets the historical [`Path::is_file`] behaviour is
/// preserved (no portable execute concept).
/// Test: `candidate_requires_execute_bit_on_unix`, plus the `resolve_binary_*`
/// tests.
fn candidate(dir: &Path, name: &str) -> Option<PathBuf> {
    let p = dir.join(name);
    if !p.is_file() {
        return None;
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        // `metadata` follows symlinks, so a symlink to an executable resolves
        // correctly. A file with no execute bit is not a runnable binary.
        match std::fs::metadata(&p) {
            Ok(meta) if meta.permissions().mode() & 0o111 != 0 => Some(p),
            _ => None,
        }
    }
    #[cfg(not(unix))]
    {
        Some(p)
    }
}

/// Path segments that mark a binary as an ephemeral (non-installed) build.
///
/// Why: a `target/debug`/`target/release` binary and any binary living under a
/// git worktree directory are rebuilt/deleted on the next `cargo` invocation or
/// worktree cleanup. Baking such a path into a persisted, shared config
/// (managed hook commands, statusline command) leaves a stale absolute path
/// that 404s once the build artifact is gone (#2229).
/// What: the worktree layout markers this workspace uses
/// (`.claude/worktrees/`, `.base/.worktrees/`) plus the two Cargo build
/// profiles. Matched as substrings so `deps/`-nested and profile-suffixed
/// variants are all covered. This list is only ONE of the two rejection rules
/// [`is_ephemeral_build_path`] applies — see [`SYSTEM_TEMP_ROOTS`] for the
/// other, and that constant's doc for why a substring list alone was not
/// enough.
const EPHEMERAL_PATH_SEGMENTS: &[&str] = &[
    "target/debug",
    "target/release",
    ".claude/worktrees/",
    ".base/.worktrees/",
];

/// Well-known absolute system temp roots, matched as component-wise path
/// PREFIXES (never substrings).
///
/// Why (#4485): [`EPHEMERAL_PATH_SEGMENTS`] enumerated build and worktree
/// layouts only, so a scratch binary living under a system temp root read as an
/// ordinary installed path and passed the guard. The Claude Code agent harness
/// puts per-session scratch space at
/// `/private/tmp/claude-<uid>/<session>/<uuid>/scratchpad/…`; a
/// `cargo test --no-run` artifact copied there was accepted as "stable" and
/// persisted into project `settings.json`, which then EXECUTED a dead libtest
/// harness on every hook event across ten unrelated projects (#4485, and via
/// `statusLine.command`, #4492). A temp root is by definition not a place an
/// installed binary lives, so nothing under one may ever be baked into a
/// persisted config.
///
/// What: the Unix temp roots in both of their macOS spellings. `/tmp` is a
/// symlink to `/private/tmp` and `/var` one to `/private/var`, so the SAME
/// directory reaches this guard under either name; both spellings are listed
/// rather than canonicalized because an ephemeral path routinely no longer
/// exists on disk and [`Path::canonicalize`] fails on exactly the paths this
/// guard most needs to reject. `/var/folders` is macOS's per-user temp root
/// (`confstr(_CS_DARWIN_USER_TEMP_DIR)`), where [`std::env::temp_dir`] lands by
/// default. The list is deliberately a set of ROOTS, not segments: matching is
/// done with [`Path::starts_with`], which compares whole path components, so
/// `/tmpfoo/bin/tm` and `/Users/x/tmp/bin/tm` are correctly NOT flagged —
/// the false-positive class a `contains("/tmp")` test would have introduced.
///
/// This is checked IN ADDITION to the live [`std::env::temp_dir`], not instead
/// of it: `temp_dir()` reflects the `TMPDIR` of the process asking the
/// question, which need not be the `TMPDIR` that produced the path being
/// judged — #4485 is precisely that case.
const SYSTEM_TEMP_ROOTS: &[&str] = &[
    "/tmp",
    "/private/tmp",
    "/var/tmp",
    "/private/var/tmp",
    "/var/folders",
    "/private/var/folders",
];

/// Whether `path` lives under a system temp root.
///
/// Why (#4485): see [`SYSTEM_TEMP_ROOTS`]. Split out of
/// [`is_ephemeral_build_path`] so the "is this a temp location" question has
/// one implementation and one place to document the prefix-vs-substring and
/// symlink-alias decisions.
/// What: returns `true` when `path` has any [`SYSTEM_TEMP_ROOTS`] entry as a
/// component-wise prefix, or is under [`std::env::temp_dir`] (also tried in its
/// canonicalized form, which resolves the macOS `/tmp` -> `/private/tmp`
/// symlink when `TMPDIR` points through it). A degenerate temp dir with no
/// parent (`/`) is ignored rather than allowed to reject every absolute path.
/// Deliberately free of `#[cfg(target_os)]`: the extra Unix roots simply never
/// match on Windows, where `temp_dir()` supplies the real answer.
///
/// (#4638) Public because a SECOND caller needs exactly this question and
/// nothing else in [`is_ephemeral_build_path`]: trusty-code's turn recorder
/// refuses to mint a memory palace for a temp-rooted project. That caller must
/// NOT use `is_ephemeral_build_path`, whose [`EPHEMERAL_PATH_SEGMENTS`] half
/// also flags `.claude/worktrees/` — a legitimate, git-remote-carrying project
/// checkout whose turns belong in the repo's real palace.
/// Test: `is_ephemeral_build_path_flags_system_temp_paths`,
/// `is_ephemeral_build_path_ignores_temp_lookalike_paths`,
/// `is_under_system_temp_is_true_for_temp_and_false_for_worktrees`.
pub fn is_under_system_temp(path: &Path) -> bool {
    if SYSTEM_TEMP_ROOTS.iter().any(|root| path.starts_with(root)) {
        return true;
    }
    let tmp = std::env::temp_dir();
    // `TMPDIR=/` would otherwise make every absolute path "ephemeral".
    if tmp.parent().is_none() {
        return false;
    }
    if path.starts_with(&tmp) {
        return true;
    }
    tmp.canonicalize()
        .is_ok_and(|real| real.parent().is_some() && path.starts_with(real))
}

/// The environment variable Cargo reads to relocate its build directory.
///
/// Why (#7244): [`EPHEMERAL_PATH_SEGMENTS`] hardcodes the DEFAULT build
/// directory's name. A build run with `CARGO_TARGET_DIR` set puts artifacts
/// somewhere that name never appears, so the substring list alone cannot see
/// them. Reading the variable lets the guard reject the build tree the ASKING
/// process is itself building into, whatever it is called.
/// What: the variable name only; [`is_under_cargo_target_dir`] does the read.
const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";

/// The two Cargo build profiles that name a build root's immediate child.
///
/// Why (#7244): the build root is what varies (`target`, `target-7224`, any
/// `CARGO_TARGET_DIR`); the profile directory under it does not. Requiring it
/// is what separates a real build tree from a directory that merely shares a
/// name — see [`is_in_cargo_build_tree`].
/// What: the profile directory names Cargo creates under a build root.
const BUILD_TREE_PROFILES: &[&str] = &["debug", "release"];

/// Path COMPONENTS Cargo creates INSIDE a build profile directory.
///
/// Why (#7244): these two names are common enough as ordinary directories
/// (`/Users/x/build/tools`, `/Users/x/deps/vendor`) that matching them on their
/// own misclassifies an installed binary as ephemeral, which stops hooks being
/// written at all — the same end-state as #7244, reached from the other
/// direction. They only mean "Cargo artifact" in the position Cargo puts them.
/// What: matched only in the [`is_in_cargo_build_tree`] positions, never alone.
const BUILD_TREE_ARTIFACT_DIRS: &[&str] = &["deps", "build"];

/// Whether `path` runs through a Cargo build-tree directory.
///
/// Why (#7244): this repo gives every agent worktree its own build directory
/// named `target-<issue>` (`target-7224`, `target-7244`). Neither
/// `"target/debug"` nor `"target/release"` is a substring of
/// `…/target-7224/debug/deps/test_session_lifecycle-<hash>`, so
/// [`is_ephemeral_build_path`] read that running TEST BINARY as a stable
/// installed path and `resolve_stable_hook_exe` baked it into the project's
/// real `settings.json` for every hook — pm-guard enforcement and Read/Bash
/// diversion were silently dead until the file was regenerated. Matching the
/// STRUCTURE Cargo produces closes the whole family rather than one more
/// literal spelling. Round 2 narrows that structure: flagging a bare `target`,
/// `deps` or `build` component wherever it appeared also rejected
/// `/Users/target/.cargo/bin/tm` and `/Users/x/build/tools/tm`, and a refused
/// path writes no hooks at all.
/// What: `true` when the path carries the Cargo LAYOUT
/// `<root>/{debug,release}[/{deps,build}]`, matched COMPONENT-wise (never as
/// substrings) in three positions: a `target` / `target-<anything>` component
/// immediately followed by a [`BUILD_TREE_PROFILES`] entry; a
/// [`BUILD_TREE_ARTIFACT_DIRS`] component whose parent is a profile (which
/// covers a `CARGO_TARGET_DIR` with no `target` in its name); or a
/// [`BUILD_TREE_ARTIFACT_DIRS`] component under a `target` / `target-*`
/// ancestor. A non-UTF-8 component matches nothing and does not collapse the
/// adjacency around it; the substring pass in [`is_ephemeral_build_path`] still
/// covers the default layout in that case.
/// Test: `is_ephemeral_build_path_flags_custom_cargo_target_dirs`,
/// `is_ephemeral_build_path_accepts_installed_paths`.
fn is_in_cargo_build_tree(path: &Path) -> bool {
    // Positions are preserved (a non-UTF-8 component stays as `None`) so a
    // component the guard cannot read never makes two others adjacent.
    let names: Vec<Option<&str>> = path.components().map(|c| c.as_os_str().to_str()).collect();
    let is_build_root =
        |n: Option<&str>| n.is_some_and(|s| s == "target" || s.starts_with("target-"));
    let is_profile = |n: Option<&str>| n.is_some_and(|s| BUILD_TREE_PROFILES.contains(&s));

    let mut under_build_root = false;
    for (i, name) in names.iter().copied().enumerate() {
        if is_build_root(name) {
            if is_profile(names.get(i + 1).copied().flatten()) {
                return true;
            }
            under_build_root = true;
        } else if name.is_some_and(|s| BUILD_TREE_ARTIFACT_DIRS.contains(&s))
            && (under_build_root || (i > 0 && is_profile(names[i - 1])))
        {
            return true;
        }
    }
    false
}

/// Whether `path` lives under the build directory `CARGO_TARGET_DIR` names.
///
/// Why (#7244): a `CARGO_TARGET_DIR` may be named anything at all — outside
/// the repo, with no `target` in its name — and [`is_in_cargo_build_tree`]
/// cannot see such a directory. When the process asking the question is itself
/// a Cargo-spawned test binary, the variable is set and names exactly the tree
/// that binary was built into, so this catches the case the structural check
/// cannot.
/// What: `true` when `path` has the variable's value as a component-wise
/// prefix, comparing both the literal value and its canonicalized form (a
/// relative or symlinked spelling of the same directory must not read as a
/// different one). The PATH is canonicalized too, but only opportunistically:
/// a build artifact is routinely already deleted, and
/// [`Path::canonicalize`] fails on exactly the paths this guard most needs to
/// reject. An unset, empty, or parent-less (`/`) value matches nothing rather
/// than flagging every absolute path.
/// Test: `is_ephemeral_build_path_flags_custom_cargo_target_dirs`.
fn is_under_cargo_target_dir(path: &Path) -> bool {
    let Some(raw) = std::env::var_os(CARGO_TARGET_DIR_ENV) else {
        return false;
    };
    if raw.is_empty() {
        return false;
    }
    let dir = PathBuf::from(raw);
    // `CARGO_TARGET_DIR=/` would otherwise make every absolute path ephemeral.
    if dir.parent().is_none() {
        return false;
    }
    if path.starts_with(&dir) {
        return true;
    }
    let Ok(real_dir) = dir.canonicalize() else {
        return false;
    };
    if real_dir.parent().is_none() {
        return false;
    }
    path.starts_with(&real_dir)
        || path
            .canonicalize()
            .is_ok_and(|real_path| real_path.starts_with(&real_dir))
}

/// Whether `path` points inside an ephemeral build/worktree/temp location that
/// will not survive a rebuild, a worktree cleanup, or a temp sweep.
///
/// Why: `std::env::current_exe()` returns a `target/debug/deps/...` path when
/// `tm`/`trusty-mpm` runs from a worktree or a debug build, and a
/// `/private/tmp/claude-<uid>/…/scratchpad/…` path when it runs from an agent
/// harness's scratch space (#4485). Persisting either into a SHARED
/// `settings.json` (hook commands, statusline) breaks every managed session
/// once the artifact is gone (#2229) — and worse, leaves config that keeps
/// EXECUTING whatever now sits at that path (#4485, #4492). Callers use this to
/// reject `current_exe()` and fall back to a stable, PATH-resolved installed
/// binary instead.
/// What: returns `true` when `path` is under a system temp root
/// ([`is_under_system_temp`]) OR its string form contains any of
/// [`EPHEMERAL_PATH_SEGMENTS`]. The segment check uses a lossy string
/// comparison so non-UTF-8 path components degrade to "not ephemeral" rather
/// than panicking; the temp check operates on components and is unaffected by
/// encoding.
/// Test: `is_ephemeral_build_path_flags_build_and_worktree_paths`,
/// `is_ephemeral_build_path_flags_system_temp_paths`,
/// `is_ephemeral_build_path_flags_custom_cargo_target_dirs`,
/// `is_ephemeral_build_path_ignores_temp_lookalike_paths`,
/// `is_ephemeral_build_path_accepts_installed_paths`.
pub fn is_ephemeral_build_path(path: &Path) -> bool {
    // #4485: a system temp root is every bit as ephemeral as `target/debug` —
    // and, unlike a build dir, an attacker-or-accident-writable location whose
    // contents a persisted hook command would go on executing.
    if is_under_system_temp(path) {
        return true;
    }
    // #7244: a renamed build directory (`target-7224/debug/deps/…`) is the same
    // artifact as `target/debug/deps/…`; the substring pass below cannot see it.
    if is_in_cargo_build_tree(path) || is_under_cargo_target_dir(path) {
        return true;
    }
    let s = path.to_string_lossy();
    EPHEMERAL_PATH_SEGMENTS.iter().any(|seg| s.contains(seg))
}

/// The cargo binary install directory: `$CARGO_HOME/bin`, falling back to
/// `~/.cargo/bin`.
///
/// Why (#4964): five call sites across `trusty-installer` and this crate each
/// re-derived this same rule, and two of them got it wrong in the same way —
/// they hardcoded `~/.cargo/bin` and never read `CARGO_HOME`, so a machine with
/// `CARGO_HOME` set resolved a directory `cargo install` does not write to.
/// One implementation means a `CARGO_HOME`-blind copy cannot be reintroduced by
/// a fifth caller.
///
/// What: reads `CARGO_HOME` from the process environment and delegates to the
/// pure [`canonical_bin_dir_from`]. Returns `None` only when `CARGO_HOME` is
/// unset/empty AND the home directory cannot be resolved. Never spawns `cargo`
/// — the resolution is pure path arithmetic, so it works on a machine with no
/// Rust toolchain installed.
///
/// Test: `canonical_bin_dir_from_*` cover the rule; this wrapper is the
/// side-effecting env read.
pub fn canonical_bin_dir() -> Option<PathBuf> {
    canonical_bin_dir_from(
        dirs::home_dir().as_deref(),
        std::env::var("CARGO_HOME").ok().as_deref(),
    )
}

/// Pure resolution of the cargo binary install directory.
///
/// Why: extracting the rule from the env/home reads makes it testable without
/// mutating process-global state, so the tests stay safe under the parallel
/// harness. It is also what lets `crate::update::candidate_bin_dirs`, which is
/// already parameterised over explicit `home`/`cargo_home` inputs, share the
/// same rule rather than restating it.
///
/// What: `<cargo_home>/bin` when `cargo_home` is `Some` and non-empty;
/// otherwise `<home>/.cargo/bin`; `None` when neither input can supply a path.
/// An empty `CARGO_HOME` is treated as unset — that is what cargo itself does,
/// and treating it literally would resolve to the relative path `bin`.
///
/// Test: `canonical_bin_dir_from_honours_cargo_home`,
/// `canonical_bin_dir_from_falls_back_to_dot_cargo`,
/// `canonical_bin_dir_from_treats_empty_cargo_home_as_unset`,
/// `canonical_bin_dir_from_is_none_without_either_input`.
pub fn canonical_bin_dir_from(home: Option<&Path>, cargo_home: Option<&str>) -> Option<PathBuf> {
    match cargo_home {
        Some(h) if !h.is_empty() => Some(PathBuf::from(h).join("bin")),
        _ => home.map(|h| h.join(".cargo").join("bin")),
    }
}

/// Crates whose `cargo install` output is NOT just `<crate_name>` — the
/// canonical crate → bin-targets table for the trusty-* workspace.
///
/// Why (#5777): the cargo ownership guard must move aside EVERY binary
/// `cargo install <crate>` will write, and the tarball allowlist must accept
/// exactly those names — a guard keyed on the crate name alone breaks for
/// alias and multi-binary crates (`tctl`, `tm`, `trusty-embedderd`, `tagent`).
/// This is the one shared table per CLAUDE.md's common-entry-point rule;
/// trusty-installer's `SIGNABLE_BINARIES` (signing sets/identifiers) and
/// `stable_set` (install-set membership) answer different questions and are
/// deliberately not folded in.
///
/// What: `(crate_name, bin targets)` pairs mirroring each crate's `[[bin]]`
/// tables (and, for trusty-search, the bundled `trusty-embedderd` binary its
/// release tarball ships). Crates absent from this table install exactly one
/// binary named after the crate — [`installed_binaries`] supplies that default.
///
/// Test: `installed_binaries_covers_multi_binary_crates`,
/// `installed_binaries_defaults_to_crate_name`.
const CRATE_BINARIES: &[(&str, &[&str])] = &[
    ("trusty-installer", &["trusty-installer", "tctl"]),
    ("trusty-mpm", &["tm", "trusty-mpm"]),
    (
        "trusty-memory",
        &["trusty-memory", "trusty-memory-mcp-bridge"],
    ),
    ("trusty-search", &["trusty-search", "trusty-embedderd"]),
    ("trusty-agents", &["tagent"]),
    ("trusty-audit", &["trusty-audit", "taudit"]),
    ("trusty-code", &["tcode"]),
];

/// The binary names `cargo install <crate_name>` writes into the bin dir.
///
/// Why (#5777): see [`CRATE_BINARIES`]. Both the cargo ownership guard
/// (`update::cargo_guard`) and trusty-installer's tarball allowlist need the
/// full per-crate binary set, so the rule lives once, here.
/// What: the table row for `crate_name`, or `[crate_name]` when the crate is
/// not listed (single binary named after the crate — tga, trusty-console, …).
/// Test: `installed_binaries_covers_multi_binary_crates`,
/// `installed_binaries_defaults_to_crate_name`.
pub fn installed_binaries(crate_name: &str) -> Vec<String> {
    CRATE_BINARIES
        .iter()
        .find(|(c, _)| *c == crate_name)
        .map(|(_, bins)| bins.iter().map(|b| (*b).to_owned()).collect())
        .unwrap_or_else(|| vec![crate_name.to_owned()])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn is_ephemeral_build_path_flags_build_and_worktree_paths() {
        for p in [
            "/Users/x/trusty-tools/target/debug/deps/trusty_mpm-abc123",
            "/Users/x/trusty-tools/target/release/tm",
            "/Users/x/repo/.claude/worktrees/fix-123/target/debug/tm",
            "/Users/x/repo/.base/.worktrees/abc/crates/trusty-mpm",
        ] {
            assert!(
                is_ephemeral_build_path(Path::new(p)),
                "{p} must be flagged as an ephemeral build path"
            );
        }
    }

    /// Why (#4485): the pre-fix guard enumerated build and worktree layouts
    /// only, so a scratch binary under a system temp root — the shape the
    /// Claude Code harness produces at
    /// `/private/tmp/claude-<uid>/<session>/<uuid>/scratchpad/base-bins/<bin>` —
    /// passed as an ordinary installed path and got persisted into
    /// `settings.json` as a hook / statusLine command that then ran a dead
    /// libtest harness on every hook event (#4492 is the same root cause via
    /// the statusLine path).
    /// What: asserts every system-temp shape is rejected: the exact harness
    /// scratchpad path, a plain `/tmp` path, the macOS `/var/folders` per-user
    /// temp root, and a path derived from the live [`std::env::temp_dir`] (so
    /// the check holds on a machine whose `TMPDIR` is none of the literals).
    /// Every shape is checked before the assertion fires, so reverting the
    /// guard reports ALL the paths it stops rejecting rather than aborting on
    /// the first — the difference between "the fix is gone" and "one case is".
    #[test]
    fn is_ephemeral_build_path_flags_system_temp_paths() {
        let temp_derived = std::env::temp_dir().join("claude-4485/scratchpad/base-bins/tm");
        let missed: Vec<String> = [
            PathBuf::from(
                "/private/tmp/claude-502/-Users-x-proj/9f1c/scratchpad/base-bins/trusty-mpm",
            ),
            PathBuf::from("/tmp/claude-502/scratchpad/base-bins/tm"),
            PathBuf::from("/tmp/tm"),
            PathBuf::from("/var/tmp/tm"),
            PathBuf::from("/var/folders/qz/9x_2/T/cargo-install-abc/tm"),
            temp_derived,
        ]
        .into_iter()
        .filter(|p| !is_ephemeral_build_path(p))
        .map(|p| p.display().to_string())
        .collect();
        assert!(
            missed.is_empty(),
            "a system temp root is never an installed location (#4485), but these were \
             accepted as stable: {missed:#?}"
        );
    }

    /// Why (#4485): the temp check must be a component-wise PREFIX match, not a
    /// `contains("/tmp")` substring match — substring matching is exactly what
    /// left the original guard incomplete, and it would newly misfire on any
    /// ordinary directory whose name merely starts with or contains `tmp`.
    /// What: asserts three lookalikes stay accepted. This test does NOT flip
    /// when the #4485 change is reverted (the old guard accepted them too); it
    /// exists to pin the matching STRATEGY against a future substring rewrite.
    #[test]
    fn is_ephemeral_build_path_ignores_temp_lookalike_paths() {
        for p in [
            "/Users/x/tmp/bin/tm",
            "/tmpfoo/bin/tm",
            "/opt/tmp-tools/bin/trusty-mpm",
        ] {
            assert!(
                !is_ephemeral_build_path(Path::new(p)),
                "{p} is not under a temp ROOT and must NOT be flagged"
            );
        }
    }

    /// Why (#4638): trusty-code's turn recorder asks THIS predicate — not
    /// [`is_ephemeral_build_path`] — whether a session's project root is
    /// durable enough to justify minting a memory palace for it. The two must
    /// stay distinguishable: a `.claude/worktrees/` checkout is an ephemeral
    /// BINARY location but a perfectly durable PROJECT (it carries the repo's
    /// git remote, so its turns belong in the repo's real palace), and
    /// conflating the two would silently stop recording every agent session.
    /// What: pins that `is_under_system_temp` flags real temp roots in both
    /// macOS spellings while accepting a worktree path that
    /// `is_ephemeral_build_path` deliberately rejects.
    #[test]
    fn is_under_system_temp_is_true_for_temp_and_false_for_worktrees() {
        for p in [
            "/private/var/folders/xx/T/.tmpAbC123/project",
            "/tmp/claude-502/session/scratchpad",
        ] {
            assert!(
                is_under_system_temp(Path::new(p)),
                "{p} is under a system temp root and must be flagged"
            );
        }

        let worktree = Path::new("/Users/x/repo/.claude/worktrees/eng-1");
        assert!(
            !is_under_system_temp(worktree),
            "a worktree checkout is a durable PROJECT root — it must not be \
             flagged as temp (#4638)"
        );
        assert!(
            is_ephemeral_build_path(worktree),
            "sanity: the same path IS an ephemeral BINARY location, which is \
             exactly why the turn recorder must not use that predicate"
        );
    }

    /// Why (#7244): `EPHEMERAL_PATH_SEGMENTS` matched the literal substrings
    /// `"target/debug"` and `"target/release"`. This repo builds each agent
    /// worktree into its own `target-<issue>` directory, whose artifact paths
    /// contain neither literal, so a running TEST BINARY at
    /// `…/target-7224/debug/deps/test_session_lifecycle-<hash>` was accepted as
    /// a stable installed path and `resolve_stable_hook_exe` wrote it into the
    /// project's real `.claude/settings.json` for every hook — pm-guard
    /// enforcement and Read/Bash diversion were dead until the file was
    /// regenerated. Same defect class as #4485 and #2229, third root cause.
    /// What: one table over the ephemeral shapes and the stable ones, asserted
    /// together so a revert reports EVERY case it stops rejecting rather than
    /// aborting on the first. The stable rows are what stop the fix from being
    /// "flag everything": an installed binary under `~/.cargo/bin` or Homebrew
    /// must still resolve, including when a symlink in the chain (a linked
    /// home, a linked `.cargo`) means the path is not the on-disk spelling.
    /// The guard reads COMPONENTS and never canonicalizes the candidate, so a
    /// symlinked prefix cannot flip the verdict — that is the property the
    /// symlinked rows pin. Round 2 adds the POSITION rows: a `target`, `build`
    /// or `deps` component only means "Cargo artifact" where Cargo puts it, so
    /// a user directory of that name stays accepted. `CARGO_TARGET_DIR` is exercised through the ambient
    /// process environment rather than a `set_var`: the test binary is itself
    /// built by Cargo, so the variable is already set to whatever build tree
    /// produced it, and mutating a process-global would race sibling tests.
    #[test]
    fn is_ephemeral_build_path_flags_custom_cargo_target_dirs() {
        let ephemeral = [
            "/Users/x/trusty-tools/target-7224/debug/deps/test_session_lifecycle-cd3ba8f0",
            "/Users/x/trusty-tools/target-7244/debug/tm",
            "/Users/x/trusty-tools/target/release/tm",
            "/Users/x/trusty-tools/target/debug/build/libsqlite3-sys-abc/build-script-build",
            // A `CARGO_TARGET_DIR` with no `target` in its name still produces
            // the `<profile>/deps` tail, which is what the guard reads.
            "/Users/x/scratch/debug/deps/trusty_mpm-1a2b3c4d",
        ];
        let stable = [
            "/Users/x/.cargo/bin/tm",
            "/opt/homebrew/bin/tm",
            // A symlinked home and a symlinked `.cargo` both produce a path
            // whose components are ordinary directory names; neither is a build
            // tree, and the guard must not follow the link to decide otherwise.
            "/Users/x/homelink/.cargo/bin/trusty-mpm",
            "/Users/x/.cargo-link/bin/tm",
            "/opt/deps-tool/bin/tm",
            "/Users/x/rebuild/bin/tm",
            // #7244 round 2: these four are the false-positive class a bare
            // component match introduced. A user directory named `target`,
            // `build` or `deps` is not a Cargo build tree, and refusing one
            // stops hooks being written at all — the same end-state as the bug
            // this guard exists to fix, reached from the other direction.
            "/Users/target/.cargo/bin/tm",
            "/Users/x/build/tools/tm",
            "/Users/x/deps/vendor/tm",
            "/Users/x/scratch/deps/trusty_mpm-1a2b3c4d",
        ];

        let missed: Vec<&str> = ephemeral
            .into_iter()
            .filter(|p| !is_ephemeral_build_path(Path::new(p)))
            .collect();
        let overreach: Vec<&str> = stable
            .into_iter()
            .filter(|p| is_ephemeral_build_path(Path::new(p)))
            .collect();

        assert!(
            missed.is_empty(),
            "a renamed Cargo build tree is still a build tree (#7244), but these \
             were accepted as stable installed paths: {missed:#?}"
        );
        assert!(
            overreach.is_empty(),
            "an installed binary must stay resolvable, but these were flagged \
             ephemeral: {overreach:#?}"
        );

        // The running test binary IS a Cargo build artifact, under whatever
        // `CARGO_TARGET_DIR` this run used. If the guard cannot see that, the
        // exact #7244 write happens again.
        let exe = std::env::current_exe().expect("current_exe resolvable under cargo test");
        assert!(
            is_ephemeral_build_path(&exe),
            "the running test binary {} is a build artifact and must never read \
             as a stable installed path (#7244)",
            exe.display()
        );
    }

    #[test]
    fn is_ephemeral_build_path_accepts_installed_paths() {
        for p in ["/Users/x/.cargo/bin/trusty-mpm", "/usr/local/bin/tm", "tm"] {
            assert!(
                !is_ephemeral_build_path(Path::new(p)),
                "{p} must NOT be flagged as an ephemeral build path"
            );
        }
    }

    #[test]
    fn daemon_path_dirs_orders_user_before_system() {
        let dirs = daemon_path_dirs();
        let pos = |needle: &str| dirs.iter().position(|p| p == &PathBuf::from(needle));
        let homebrew = pos("/opt/homebrew/bin").expect("homebrew dir present");
        let usr_bin = pos("/usr/bin").expect("/usr/bin present");
        assert!(
            homebrew < usr_bin,
            "Homebrew must precede /usr/bin so it shadows older system copies"
        );
    }

    #[test]
    fn daemon_path_dirs_expands_home() {
        let home = dirs::home_dir().expect("home dir resolvable in test env");
        let dirs = daemon_path_dirs();
        assert!(
            dirs.contains(&home.join(".local/bin")),
            "~/.local/bin must be expanded to the real home"
        );
        assert!(
            dirs.contains(&home.join(".cargo/bin")),
            "~/.cargo/bin must be expanded to the real home"
        );
        // No literal tilde should survive expansion.
        assert!(
            dirs.iter().all(|p| !p.starts_with("~")),
            "launchd does not expand ~; paths must be absolute"
        );
    }

    #[test]
    fn daemon_path_dirs_dedupes() {
        let dirs = daemon_path_dirs();
        let mut sorted = dirs.clone();
        sorted.sort();
        sorted.dedup();
        assert_eq!(
            sorted.len(),
            dirs.len(),
            "daemon_path_dirs must not contain duplicates"
        );
    }

    #[test]
    fn daemon_path_env_contains_expected_dirs() {
        let env = daemon_path_env();
        let home = dirs::home_dir().expect("home dir resolvable in test env");
        assert!(env.contains("/opt/homebrew/bin"), "PATH missing Homebrew");
        assert!(
            env.contains("/usr/local/bin"),
            "PATH missing /usr/local/bin"
        );
        assert!(
            env.contains(home.join(".local/bin").to_str().unwrap()),
            "PATH missing expanded ~/.local/bin"
        );
        assert!(
            env.contains(home.join(".cargo/bin").to_str().unwrap()),
            "PATH missing expanded ~/.cargo/bin"
        );
        for sys in SYSTEM_BIN_DIRS {
            assert!(env.contains(sys), "PATH missing system dir {sys}");
        }
    }

    /// Create a unique temp directory for a test, returning its path.
    ///
    /// Why: each test needs an isolated scratch dir; keying it on the test name
    /// plus the process id avoids collisions under the parallel harness without
    /// pulling in an extra dev-dependency.
    /// What: joins the system temp dir with `bin_resolve_<tag>_<pid>` and
    /// `create_dir_all`s it.
    /// Test: exercised by every test that calls it.
    fn make_temp_dir(tag: &str) -> PathBuf {
        let dir = std::env::temp_dir().join(format!("bin_resolve_{tag}_{}", std::process::id()));
        std::fs::create_dir_all(&dir).expect("create temp dir");
        dir
    }

    /// Write a file and, on Unix, mark it executable.
    ///
    /// Why: [`candidate`] now requires the execute bit on Unix, so test fixtures
    /// that stand in for binaries must be chmod'd `0o755` to be discoverable.
    /// What: writes a tiny shebang stub at `path` and sets mode `0o755` on Unix.
    /// Test: exercised by `candidate_*` and `resolve_binary_*` tests.
    fn write_executable(path: &Path) {
        std::fs::write(path, b"#!/bin/sh\n").expect("write fixture");
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = std::fs::metadata(path).expect("stat fixture").permissions();
            perms.set_mode(0o755);
            std::fs::set_permissions(path, perms).expect("chmod fixture");
        }
    }

    #[test]
    fn resolve_binary_finds_in_well_known_dir() {
        // Verify the "found in a directory" branch WITHOUT mutating the
        // process-global PATH (which races the parallel test harness). We
        // exercise candidate() directly against an explicit temp dir — the same
        // per-directory predicate resolve_binary() applies to each PATH and
        // well-known-dir entry — and confirm resolve_binary() accepts the
        // resulting explicit path verbatim.
        let tmp = make_temp_dir("well_known");
        let bin = tmp.join("fake-tool-xyz");
        write_executable(&bin);

        // candidate() finds the executable given its directory.
        let hit = candidate(&tmp, "fake-tool-xyz");
        assert_eq!(hit.as_deref(), Some(bin.as_path()));

        // resolve_binary() accepts the discovered path as an explicit path,
        // closing the loop from "found in a dir" to "usable result" — no PATH
        // mutation required.
        let explicit = bin.to_str().expect("utf8 temp path");
        assert_eq!(resolve_binary(explicit).as_deref(), Some(bin.as_path()));

        std::fs::remove_dir_all(&tmp).ok();
    }

    /// Why (#4125): this is the branch that makes a launchd-spawned daemon
    /// able to run a binary its inherited `PATH` never mentions —
    /// `/opt/homebrew/bin/uv` being the case that broke the py-embedder
    /// bootstrap, because `locate_uv` used a bare `which::which("uv")` with no
    /// fallback at all. Nothing previously asserted that fallback works:
    /// `resolve_binary_finds_in_well_known_dir` avoids `PATH` mutation and so
    /// only reaches [`candidate`] and the explicit-path branch.
    /// What: plants an executable in a temp dir that is NOT on the process
    /// `PATH`, confirms a plain [`resolve_binary`] therefore misses it (the
    /// `which`-equivalent answer), then confirms [`resolve_binary_in`] finds it
    /// once that dir is in the fallback list. Against a PATH-only resolver the
    /// second assertion fails.
    /// Test: this test.
    #[test]
    fn resolve_binary_finds_a_binary_outside_the_process_path() {
        let tmp = make_temp_dir("outside_path");
        let name = "trusty-fake-uv-4125";
        let bin = tmp.join(name);
        write_executable(&bin);

        assert!(
            resolve_binary(name).is_none(),
            "fixture dir must not be on the process PATH for this test to mean anything"
        );
        assert_eq!(
            resolve_binary_in(name, std::slice::from_ref(&tmp)).as_deref(),
            Some(bin.as_path()),
            "a binary outside the process PATH must still resolve from the fallback dirs"
        );

        std::fs::remove_dir_all(&tmp).ok();
    }

    #[cfg(unix)]
    #[test]
    fn candidate_requires_execute_bit_on_unix() {
        use std::os::unix::fs::PermissionsExt;

        let tmp = make_temp_dir("exec_bit");

        // A non-executable regular file must NOT be returned.
        let data = tmp.join("not-a-binary");
        std::fs::write(&data, b"plain data\n").expect("write data file");
        let mut perms = std::fs::metadata(&data).expect("stat data").permissions();
        perms.set_mode(0o644);
        std::fs::set_permissions(&data, perms).expect("chmod data");
        assert_eq!(
            candidate(&tmp, "not-a-binary"),
            None,
            "a non-executable regular file must not resolve as a runnable binary"
        );

        // An executable file with the same parent dir IS returned.
        let exe = tmp.join("a-binary");
        write_executable(&exe);
        assert_eq!(
            candidate(&tmp, "a-binary").as_deref(),
            Some(exe.as_path()),
            "an executable file must resolve"
        );

        std::fs::remove_dir_all(&tmp).ok();
    }

    #[test]
    fn resolve_binary_returns_none_for_missing() {
        assert!(
            resolve_binary("definitely-not-a-real-binary-zzz-1298").is_none(),
            "a nonexistent binary must resolve to None"
        );
    }

    #[test]
    fn resolve_binary_accepts_absolute_path() {
        // /bin/sh exists on every supported unix.
        let sh = PathBuf::from("/bin/sh");
        if sh.is_file() {
            assert_eq!(resolve_binary("/bin/sh"), Some(sh));
        }
        assert!(
            resolve_binary("/no/such/path/here-1298").is_none(),
            "a non-existent explicit path must resolve to None"
        );
    }

    /// Why (#4964): the whole reason this helper exists is that two of the
    /// five copies it replaces ignored `CARGO_HOME` and hardcoded
    /// `~/.cargo/bin`. A `CARGO_HOME` that is honoured is the load-bearing
    /// behaviour.
    /// What: a non-empty `CARGO_HOME` wins over `home` entirely.
    /// Test: this is the test.
    #[test]
    fn canonical_bin_dir_from_honours_cargo_home() {
        let got = canonical_bin_dir_from(Some(Path::new("/home/u")), Some("/opt/ch"));
        assert_eq!(got, Some(PathBuf::from("/opt/ch/bin")));
    }

    /// Why: with no `CARGO_HOME`, cargo installs into `~/.cargo/bin`.
    /// What: `None` cargo_home falls back to `<home>/.cargo/bin`.
    /// Test: this is the test.
    #[test]
    fn canonical_bin_dir_from_falls_back_to_dot_cargo() {
        let got = canonical_bin_dir_from(Some(Path::new("/home/u")), None);
        assert_eq!(got, Some(PathBuf::from("/home/u/.cargo/bin")));
    }

    /// Why: `CARGO_HOME=""` is how a shell exports a variable it never set a
    /// value for. Taken literally it resolves to the RELATIVE path `bin`,
    /// which would place binaries under the process's working directory.
    /// What: an empty `CARGO_HOME` resolves the same as an absent one.
    /// Test: this is the test.
    #[test]
    fn canonical_bin_dir_from_treats_empty_cargo_home_as_unset() {
        let got = canonical_bin_dir_from(Some(Path::new("/home/u")), Some(""));
        assert_eq!(got, Some(PathBuf::from("/home/u/.cargo/bin")));
    }

    /// Why: with neither input there is no defensible guess; callers must see
    /// `None` and decide, rather than receive a fabricated relative path.
    /// What: both inputs absent → `None`.
    /// Test: this is the test.
    #[test]
    fn canonical_bin_dir_from_is_none_without_either_input() {
        assert_eq!(canonical_bin_dir_from(None, None), None);
        assert_eq!(canonical_bin_dir_from(None, Some("")), None);
    }

    /// Why (#5777): the alias/multi-binary crates are exactly the ones the
    /// cargo ownership guard existed to not break — a table row silently
    /// dropping one of them reintroduces the exit-101 collision on that
    /// alias. An earlier version of this test hard-coded a verbatim mirror of
    /// the table, which a future row edit would sail past; two independent
    /// review rounds flagged that (#5778 review).
    /// What: iterates [`CRATE_BINARIES`] itself, so every row — present and
    /// future — is exercised through [`installed_binaries`]; then pins the
    /// four ticket-named alias binaries the table must never lose, the one
    /// drift a lookup-only iteration cannot catch (a deleted row).
    #[test]
    fn installed_binaries_covers_multi_binary_crates() {
        for (krate, expected) in CRATE_BINARIES {
            assert_eq!(
                installed_binaries(krate),
                expected.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>(),
                "binary set for {krate} must come from its CRATE_BINARIES row"
            );
        }
        // #5777 hard requirements: these aliases are what a crate-name-only
        // guard breaks on; deleting a table row must fail here, loudly.
        for (krate, alias) in [
            ("trusty-installer", "tctl"),
            ("trusty-mpm", "tm"),
            ("trusty-search", "trusty-embedderd"),
            ("trusty-agents", "tagent"),
        ] {
            assert!(
                installed_binaries(krate).iter().any(|b| b == alias),
                "{krate} must install `{alias}` (#5777 hard requirement)"
            );
        }
    }

    /// Why: a crate absent from the table installs one binary named after
    /// itself; the fallback must supply that rather than an empty set (an
    /// empty set would make the guard and the allowlist silently vacuous).
    /// What: unlisted crates map to `[crate_name]`.
    #[test]
    fn installed_binaries_defaults_to_crate_name() {
        assert_eq!(installed_binaries("tga"), vec!["tga".to_owned()]);
        assert_eq!(
            installed_binaries("trusty-console"),
            vec!["trusty-console".to_owned()]
        );
    }
}