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
//! REAL-git coverage for the sweep's #7889 landed-content admission.
//!
//! Why: the gate-ladder tests in `worktree_reclaim_tests` inject a clean dirt
//! probe, so they cannot see what `inspect_dirt` really reports for a donor
//! branch. These build that branch — its commit reaches no `origin` ref, and
//! the squash that landed it also carries a sibling's work — and run the real
//! probes over it. Every fixture lives in a `tempfile::TempDir`.
//!
//! What: the survey admits the donor tree; the sweep probe refuses a donor
//! tree holding an uncommitted file; the pre-delete re-check admits a landed
//! tree with no pull request and refuses one that stopped being landed. The
//! #7771 (f) route counts a tree landed only when every commit is on some
//! origin ref, and refuses when that count fails. #8721: a detached HEAD
//! whose content landed is admitted by the survey and the re-check; residue,
//! and `Unknown` on a branch, still refuse.

use std::path::Path;

use super::reclaim_landed_content;
use crate::core::worktree_landed_content::LandedContent;
use crate::session_manager::worktree_git_fixture::GitWorktreeFixture;
use crate::session_manager::worktree_ownership::{AgentDelegationState, AgentWorktreeOwner};
use crate::session_manager::worktree_reclaim::{
    BranchPrState, KeepList, LiveClaims, PrIndex, ReclaimVerdict,
};
use crate::session_manager::worktree_reclaim_sweep::{
    SurveyBudget, recheck_before_delete, survey_with_landed_content,
};

/// The strictest agent probe, as `worktree_reclaim_sweep_tests` uses it.
fn no_agents(_: &AgentWorktreeOwner) -> AgentDelegationState {
    AgentDelegationState::Unknown
}

/// A complete PR index that carries only an unrelated branch, so the donor
/// branch resolves to `NoPr` without a per-branch `gh` call.
fn unrelated_index(_: &Path) -> PrIndex {
    PrIndex::from_json(
        r#"[{"number": 72, "headRefName": "session/somebody-else", "state": "MERGED"}]"#,
        400,
    )
}

/// A worktree in the real #7889 donor shape, returned with its fixture.
fn donor(name: &str) -> (GitWorktreeFixture, std::path::PathBuf) {
    let fx = GitWorktreeFixture::new();
    let wt = fx.add_worktree(name);
    fx.land_with_sibling_work(&wt, &format!("{name}.txt"), &format!("{name}-r2.txt"));
    (fx, wt)
}

/// 🔴 REGRESSION (#7889): the survey admits a real donor branch. Gate 6's
/// `inspect_dirt` counts its commit as unpushed, because no patch id on
/// `origin/main` matches it; the content comparison finds nothing to land.
///
/// Fails before the fix: `no_pr_verdict` refused on that commit count, so the
/// verdict was `Blocked { gate: UnsavedWork, .. }` and the admission never ran.
#[test]
fn worktree_7889_the_sweep_admits_a_real_donor_branch() {
    let (fx, wt) = donor("donor-7889");
    let s = survey_with_landed_content(
        &fx.repos_root,
        &LiveClaims::default(),
        &unrelated_index,
        &no_agents,
        SurveyBudget::default(),
        false,
        &KeepList::default(),
        &[],
        Some(&reclaim_landed_content),
    );
    let found = s
        .candidates
        .iter()
        .find(|c| c.path == wt)
        .unwrap_or_else(|| panic!("survey missed {}", wt.display()));
    assert!(
        matches!(
            found.verdict,
            ReclaimVerdict::ReclaimableLandedContent { .. }
        ),
        "a donor tree whose content is on origin/main must be reclaimable: {:?}",
        found.verdict
    );
}

/// 🔴 #7889: the probe runs the dirt checks `inspect_dirt` skipped. An
/// untracked file is not in HEAD, so no comparison of HEAD can vouch for it.
///
/// Fails without the probe's own dirty-file check: the content comparison
/// alone reports `Landed` for this tree.
#[test]
fn worktree_7889_the_sweep_probe_refuses_an_uncommitted_file() {
    let (_fx, wt) = donor("donor-dirty-7889");
    std::fs::write(wt.join("notes.md"), "never committed\n").expect("write untracked file");
    let verdict = reclaim_landed_content(&wt);
    assert!(
        !verdict.admits(),
        "an uncommitted file must refuse: {verdict:?}"
    );
    assert!(verdict.note().contains("uncommitted"), "{}", verdict.note());

    // A path git cannot answer for fails the dirty-check itself, and refuses.
    let tmp = tempfile::tempdir().expect("tempdir");
    assert!(!reclaim_landed_content(tmp.path()).admits());
}

/// 🔴 REGRESSION (#7889): the pre-delete re-check admits a landed tree with no
/// pull request, so an approved candidate can actually be removed.
///
/// Fails before the fix: the re-check refused every state but `Merged`, so
/// every candidate the survey admitted on content was refused here.
#[test]
fn worktree_7889_the_recheck_admits_a_landed_tree_with_no_pull_request() {
    let (_fx, wt) = donor("donor-recheck-7889");
    assert_eq!(
        recheck_before_delete(
            &wt,
            &KeepList::default(),
            Some(&LiveClaims::default()),
            &BranchPrState::NoPr,
            &no_agents,
            Some(&reclaim_landed_content),
        ),
        None
    );
}

/// 🔴 #7889: the re-check re-asks the evidence. A commit made after the survey
/// is residue, so the re-check refuses and names the admission. Offering no
/// probe keeps the pre-#7889 refusal.
#[test]
fn worktree_7889_the_recheck_refuses_a_tree_no_longer_landed() {
    let (_fx, wt) = donor("donor-residue-7889");
    GitWorktreeFixture::commit_unpushed(&wt);
    let reason = recheck_before_delete(
        &wt,
        &KeepList::default(),
        Some(&LiveClaims::default()),
        &BranchPrState::NoPr,
        &no_agents,
        Some(&reclaim_landed_content),
    )
    .expect("residue must refuse");
    assert!(reason.contains("landed-content"), "{reason}");
    assert!(reason.contains("unpushed.txt"), "{reason}");

    let unoffered = recheck_before_delete(
        &wt,
        &KeepList::default(),
        Some(&LiveClaims::default()),
        &BranchPrState::NoPr,
        &no_agents,
        None,
    )
    .expect("no probe offered must refuse");
    assert!(unoffered.contains("no longer a merge"), "{unoffered}");
}

/// Run `git -C <dir> <args>`, panicking with git's stderr on failure.
fn git(dir: &Path, args: &[&str]) {
    let out = std::process::Command::new("git")
        .arg("-C")
        .arg(dir)
        .args(args)
        .output()
        .expect("fixture: git could not be run");
    assert!(
        out.status.success(),
        "fixture: `git {}` failed: {}",
        args.join(" "),
        String::from_utf8_lossy(&out.stderr)
    );
}

/// 🔴 REGRESSION (#7889, critic CRITICAL): a commit on `session/<leaf>` that
/// HEAD cannot reach is counted by gate 6 as an unpushed commit with no dirty
/// file, so it reached the admission — which judges HEAD only, found HEAD on
/// `origin/main`, and granted. The removal then deletes `session/<leaf>` and
/// with it the commit's last ref.
///
/// Fails at bc9df345a: the verdict there is `ReclaimableLandedContent`.
#[test]
fn worktree_7889_a_session_branch_commit_head_cannot_reach_refuses() {
    let fx = GitWorktreeFixture::new();
    let wt = fx.add_worktree("moved-7889");
    GitWorktreeFixture::commit_unpushed(&wt);
    let orphan = std::process::Command::new("git")
        .arg("-C")
        .arg(&wt)
        .args(["rev-parse", "HEAD"])
        .output()
        .expect("rev-parse");
    let orphan = String::from_utf8_lossy(&orphan.stdout).trim().to_string();
    git(&wt, &["switch", "-c", "donor-moved", "origin/main"]);
    let s = survey_with_landed_content(
        &fx.repos_root,
        &LiveClaims::default(),
        &unrelated_index,
        &no_agents,
        SurveyBudget::default(),
        false,
        &KeepList::default(),
        &[],
        Some(&reclaim_landed_content),
    );
    let found = s
        .candidates
        .iter()
        .find(|c| c.path == wt)
        .unwrap_or_else(|| panic!("survey missed {}", wt.display()));
    assert!(
        !found.verdict.is_reclaimable(),
        "a commit only session/moved-7889 holds must refuse: {:?}",
        found.verdict
    );
    // #7889 critic round 2: the refusal names the branch and the commit.
    let shown = format!("{:?}", found.verdict);
    assert!(shown.contains("`session/moved-7889`"), "{shown}");
    assert_eq!(orphan.len(), 40, "premise: a full sha: {orphan}");
    assert!(shown.contains(&orphan), "the first unlanded sha: {shown}");
}

/// 🔴 REGRESSION (#7889, critic MEDIUM): the admission can take 40 s, so a
/// grant is re-checked. A file written, or a commit made, while it ran refuses.
///
/// Fails at bc9df345a, which returned the admission's grant unexamined.
#[test]
fn worktree_7889_dirt_that_appears_during_the_admission_refuses() {
    let landed = || -> crate::core::worktree_landed_content::LandingAdmission {
        LandedContent::Landed {
            base: "origin/main".into(),
            base_sha: "0".repeat(40),
            landed_at: None,
        }
        .into()
    };

    let (_fx, wt) = donor("donor-late-file-7889");
    let late_file = super::reclaim_landed_content_with(&wt, &|p| {
        std::fs::write(p.join("late.txt"), "written mid-admission\n").expect("write");
        landed()
    });
    assert!(!late_file.admission.admits(), "{late_file:?}");
    assert!(
        late_file.admission.note().contains("appeared while"),
        "{}",
        late_file.admission.note()
    );

    let (_fx2, wt2) = donor("donor-late-commit-7889");
    let late_commit = super::reclaim_landed_content_with(&wt2, &|p| {
        GitWorktreeFixture::commit_unpushed(p);
        landed()
    });
    assert!(!late_commit.admission.admits(), "{late_commit:?}");
    assert!(
        late_commit.admission.note().contains("HEAD moved"),
        "{}",
        late_commit.admission.note()
    );
}

/// 🔴 REGRESSION (#7889): a donor matched to its sibling's merged pull request
/// by the #7267 commit search still carries commits `inspect_dirt` counts as
/// unpushed. The re-check asks the admission about them rather than refusing.
///
/// Fails before the fix: a merge re-ran `inspect_dirt` alone, which refused
/// this tree on its donor commit, so the candidate was never removed.
#[test]
fn worktree_7889_the_recheck_admits_a_merged_donor_with_commits_only_dirt() {
    let (_fx, wt) = donor("donor-merged-7889");
    let merged = BranchPrState::Merged { pr: 8328 };
    assert_eq!(
        recheck_before_delete(
            &wt,
            &KeepList::default(),
            Some(&LiveClaims::default()),
            &merged,
            &no_agents,
            Some(&reclaim_landed_content),
        ),
        None
    );
    let unoffered = recheck_before_delete(
        &wt,
        &KeepList::default(),
        Some(&LiveClaims::default()),
        &merged,
        &no_agents,
        None,
    )
    .expect("no probe offered keeps the pre-#7889 refusal");
    assert!(unoffered.contains("unpushed"), "{unoffered}");
}

/// A tree under `.claude/worktrees/` holding one commit that `origin` carries
/// only under a DIFFERENT branch name (#7771).
fn published_elsewhere(fx: &GitWorktreeFixture, name: &str) -> std::path::PathBuf {
    let wt = fx.add_worktree_at(&fx.repo.join(".claude").join("worktrees"), name);
    std::fs::write(wt.join("published.txt"), "on origin\n").expect("write");
    git(&wt, &["add", "published.txt"]);
    git(&wt, &["commit", "-q", "-m", "published elsewhere"]);
    let refspec = format!("HEAD:refs/heads/vc/{name}");
    git(&wt, &["push", "-q", "origin", &refspec]);
    git(&wt, &["fetch", "-q", "origin"]);
    wt
}

/// 🔴 REGRESSION (#7771 f): a commit on ANY origin ref counts, and one commit
/// on no origin ref beside it makes the tree unlanded.
///
/// The second half uses a clean dirt probe, so only the origin count can
/// refuse: an implementation asking "is any commit on origin" passes the first
/// half and fails here.
#[test]
fn worktree_7771_published_needs_every_commit_on_some_origin_ref() {
    use super::{landing_recheck, published_verdict};
    use crate::session_manager::worktree_safety::inspect_dirt;
    let fx = GitWorktreeFixture::new();
    let wt = published_elsewhere(&fx, "published-elsewhere-7771");
    assert!(
        published_verdict(&wt, &inspect_dirt).is_some(),
        "every commit is on origin/vc/published-elsewhere-7771"
    );
    assert_eq!(landing_recheck(&wt, &BranchPrState::NoPr, None), None);

    GitWorktreeFixture::commit_unpushed(&wt);
    assert!(
        published_verdict(&wt, &|_| None).is_none(),
        "one commit on origin and one local-only is not landed"
    );
    assert!(landing_recheck(&wt, &BranchPrState::NoPr, None).is_some());
}

/// #7771 (f) error arm: when the origin count cannot run, the tree is not
/// published, so the caller's refusal stands (ADR-0045).
///
/// The tree's `HEAD` names an unborn branch, so `git rev-list HEAD` fails
/// inside a real worktree; a plain directory fails the same count outright.
#[test]
fn worktree_7771_a_failed_origin_count_refuses() {
    use super::{landing_recheck, published_verdict};
    use crate::core::worktree_removal_facts::LOCAL_ONLY_COMMITS_ARGS;
    use crate::session_manager::worktree_safety::git_stdout;
    let fx = GitWorktreeFixture::new();
    let wt = published_elsewhere(&fx, "count-fails-7771");
    assert!(published_verdict(&wt, &|_| None).is_some(), "precondition");
    git(&wt, &["symbolic-ref", "HEAD", "refs/heads/unborn-7771"]);
    assert!(
        git_stdout(&wt, LOCAL_ONLY_COMMITS_ARGS).is_err(),
        "the count must fail for this test to reach the error arm"
    );
    assert!(published_verdict(&wt, &|_| None).is_none());
    assert!(landing_recheck(&wt, &BranchPrState::Unknown, None).is_some());

    let plain = tempfile::tempdir().expect("tempdir");
    assert!(published_verdict(plain.path(), &|_| None).is_none());
}

/// The #8721 shape: a donor tree detached on its landed commit, its branch
/// deleted, so the survey resolves it to `BranchPrState::Unknown`.
fn detached_donor(name: &str) -> (GitWorktreeFixture, std::path::PathBuf) {
    let (fx, wt) = donor(name);
    git(&wt, &["checkout", "-q", "--detach"]);
    git(&wt, &["branch", "-q", "-D", &format!("session/{name}")]);
    (fx, wt)
}

/// The survey's verdict for `wt` under `index`, with `probe` offered.
fn survey_verdict(
    fx: &GitWorktreeFixture,
    wt: &Path,
    index: &dyn Fn(&Path) -> PrIndex,
    probe: &dyn Fn(&Path) -> crate::core::worktree_landed_content::LandingAdmission,
) -> (BranchPrState, ReclaimVerdict) {
    survey_verdict_with(fx, wt, index, probe, false)
}

/// [`survey_verdict`]; `fallback` runs the `gh` commit search, as `--merged-prs`
/// does. Here it cannot answer: `origin` is a local path, not a GitHub slug.
fn survey_verdict_with(
    fx: &GitWorktreeFixture,
    wt: &Path,
    index: &dyn Fn(&Path) -> PrIndex,
    probe: &dyn Fn(&Path) -> crate::core::worktree_landed_content::LandingAdmission,
    fallback: bool,
) -> (BranchPrState, ReclaimVerdict) {
    let s = survey_with_landed_content(
        &fx.repos_root,
        &LiveClaims::default(),
        index,
        &no_agents,
        SurveyBudget::default(),
        fallback,
        &KeepList::default(),
        &[],
        Some(probe),
    );
    let found = s
        .candidates
        .into_iter()
        .find(|c| c.path == wt)
        .unwrap_or_else(|| panic!("survey missed {}", wt.display()));
    (found.pr, found.verdict)
}

/// 🔴 REGRESSION (#8721): a clean detached tree whose content is on
/// `origin/main` is reclaimable. Fails at 62b6f29e1, where gate 5 refused
/// every `Unknown` state without asking the admission.
#[test]
fn worktree_8721_the_sweep_admits_a_detached_head_whose_content_landed() {
    let (fx, wt) = detached_donor("detached-8721");
    let (pr, verdict) = survey_verdict(&fx, &wt, &unrelated_index, &reclaim_landed_content);
    assert_eq!(pr, BranchPrState::Unknown, "fixture: detached");
    assert!(
        matches!(verdict, ReclaimVerdict::ReclaimableLandedContent { .. }),
        "{verdict:?}"
    );
}

/// 🔴 #8721 error arm: a detached tree holding a commit `origin/main` lacks is
/// refused, and the refusal names the admission. Fails at 62b6f29e1, whose
/// refusal never asked it.
#[test]
fn worktree_8721_the_sweep_refuses_a_detached_head_holding_residue() {
    let (fx, wt) = detached_donor("detached-residue-8721");
    GitWorktreeFixture::commit_unpushed(&wt);
    let (_, verdict) = survey_verdict(&fx, &wt, &unrelated_index, &reclaim_landed_content);
    assert!(!verdict.is_reclaimable(), "{verdict:?}");
    let shown = format!("{verdict:?}");
    assert!(shown.contains("landed-content"), "{shown}");
    assert!(shown.contains("unpushed.txt"), "{shown}");
}

/// 🔴 #8721: `Unknown` on a BRANCH — a truncated index — never reaches the
/// admission, even when the probe would admit.
#[test]
fn worktree_8721_an_unknown_state_on_a_branch_is_still_refused() {
    let (fx, wt) = donor("truncated-8721");
    // A full page (one row, limit one) is read as truncated.
    let truncated = |_: &Path| {
        PrIndex::from_json(
            r#"[{"number": 72, "headRefName": "session/somebody-else", "state": "MERGED"}]"#,
            1,
        )
    };
    let landed = |_: &Path| -> crate::core::worktree_landed_content::LandingAdmission {
        LandedContent::Landed {
            base: "origin/main".into(),
            base_sha: "0".repeat(40),
            landed_at: None,
        }
        .into()
    };
    let (pr, verdict) = survey_verdict(&fx, &wt, &truncated, &landed);
    assert_eq!(pr, BranchPrState::Unknown, "fixture: truncated index");
    assert!(!verdict.is_reclaimable(), "{verdict:?}");
}

/// 🔴 #8721: the pre-delete re-check admits the detached tree the survey did.
#[test]
fn worktree_8721_the_recheck_admits_a_detached_head_whose_content_landed() {
    let (_fx, wt) = detached_donor("detached-recheck-8721");
    let recheck = |probe| {
        let unknown = BranchPrState::Unknown;
        let claims = LiveClaims::default();
        recheck_before_delete(
            &wt,
            &KeepList::default(),
            Some(&claims),
            &unknown,
            &no_agents,
            probe,
        )
    };
    assert_eq!(recheck(Some(&reclaim_landed_content)), None);
    assert!(
        recheck(None).is_some(),
        "no probe offered keeps the refusal"
    );
}

/// 🔴 FAIL-OPEN CHECK (#8721, critic HIGH): a commit search that did not
/// answer on a detached tree whose content IS on `origin/main` never reaches
/// the landed-content admission — at the survey or at the pre-delete
/// re-check — because the failure is `LookupFailed`, not `Unknown`. Fails at
/// 334329d3e, which resolved it to `Unknown` and reclaimed the tree. A
/// published detached tree still takes #7771 (f)'s route, which needs no
/// GitHub answer.
#[test]
fn worktree_8721_a_failed_commit_search_never_reclaims_a_detached_head() {
    let (fx, wt) = detached_donor("search-failed-8721");
    let (pr, verdict) =
        survey_verdict_with(&fx, &wt, &unrelated_index, &reclaim_landed_content, true);
    assert!(!verdict.is_reclaimable(), "{verdict:?}");
    assert!(
        matches!(pr, BranchPrState::LookupFailed { .. }),
        "the search did not answer: {pr:?}"
    );
    assert!(
        !format!("{verdict:?}").contains("landed-content"),
        "{verdict:?}"
    );
    let claims = LiveClaims::default();
    let recheck = |p: &BranchPrState| {
        let offered: &dyn Fn(&Path) -> _ = &reclaim_landed_content;
        recheck_before_delete(
            &wt,
            &KeepList::default(),
            Some(&claims),
            p,
            &no_agents,
            Some(offered),
        )
    };
    assert!(recheck(&pr).is_some(), "the re-check refuses it too");

    let published = published_elsewhere(&fx, "published-detached-8721");
    git(&published, &["checkout", "-q", "--detach"]);
    // No probe offered: only the #7771 (f) route can permit, and it does.
    assert_eq!(super::landing_recheck(&published, &pr, None), None);
}