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
//! Unit tests for the cold-start entry point (#4990).
//!
//! Why: the two reuse behaviours are the whole reason this module exists, and
//! they are deliberately DIFFERENT — a wrong remote refuses, a dirty tree warns
//! and proceeds. Both are proven against REAL temp git repositories rather than
//! mocks, because each one lives entirely in what git reports about a working
//! directory. The remote canonicalization is pure and gets ordinary table
//! tests.
//! What: `canonical_remote` equivalence and non-equivalence, including SSH
//! alias resolution from an injected table (#9089); the
//! remote-mismatch and no-origin refusals; the dirty-tree warn-and-proceed
//! (including that the fetch still lands and the local branch does not move);
//! the clean-reuse fast-forward; the fresh clone; and the truncation of a long
//! dirty-status notice.
//! Test: this file IS the test module.

use std::path::Path;
use std::process::Command;

use super::*;

/// Run a git command in `dir`, panicking with full output on failure.
fn git(dir: &Path, args: &[&str]) -> String {
    let out = Command::new("git")
        .arg("-C")
        .arg(dir)
        .args(args)
        .output()
        .unwrap_or_else(|e| panic!("failed to spawn git {args:?} in {dir:?}: {e}"));
    assert!(
        out.status.success(),
        "git {args:?} in {dir:?} failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    String::from_utf8_lossy(&out.stdout).trim().to_string()
}

/// Create a one-commit `main` repo at `path` and return it.
fn init_origin(path: &Path) -> &Path {
    std::fs::create_dir_all(path).expect("mkdir origin");
    git(path, &["init", "-q", "-b", "main"]);
    git(path, &["config", "user.email", "test@example.com"]);
    git(path, &["config", "user.name", "Test"]);
    std::fs::write(path.join("file.txt"), "v1\n").expect("write file");
    git(path, &["add", "."]);
    git(path, &["commit", "-q", "-m", "initial"]);
    path
}

/// Clone `origin` to `dest` so `dest` has a real `origin` remote and `origin/HEAD`.
fn clone_to(origin: &Path, dest: &Path) {
    let out = Command::new("git")
        .args([
            "clone",
            "-q",
            origin.to_str().expect("utf8"),
            dest.to_str().expect("utf8"),
        ])
        .output()
        .expect("spawn git clone");
    assert!(
        out.status.success(),
        "git clone failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    git(dest, &["config", "user.email", "test@example.com"]);
    git(dest, &["config", "user.name", "Test"]);
}

/// Why: the same repository is spelled three ways in the wild, and a mismatch
/// error for any pair of them would fire on every ordinary GitHub setup.
/// Test: itself.
#[test]
fn equivalent_remote_spellings_match() {
    let want = "github.com/bobmatnyc/trusty-tools";
    for url in [
        "git@github.com:bobmatnyc/trusty-tools.git",
        "git@github.com:bobmatnyc/trusty-tools",
        "https://github.com/bobmatnyc/trusty-tools",
        "https://github.com/bobmatnyc/trusty-tools.git",
        "https://github.com/bobmatnyc/trusty-tools/",
        "ssh://git@github.com/bobmatnyc/trusty-tools.git",
        "HTTPS://GitHub.com/BobMatNYC/Trusty-Tools.git",
    ] {
        assert_eq!(canonical_remote(url), want, "canonicalizing {url}");
    }
}

/// The #9089 repro's alias table, parsed from a literal — never `~/.ssh/config`.
fn duetto_aliases() -> SshHostAliases {
    SshHostAliases::parse("Host github-duetto\n  HostName github.com\n")
}

/// A checkout at `<tmp>/base` whose `origin` is the #9089 alias remote.
fn alias_origin_checkout(tmp: &Path) -> std::path::PathBuf {
    let base = init_origin(&tmp.join("base")).to_path_buf();
    git(
        &base,
        &[
            "remote",
            "add",
            "origin",
            "git@github-duetto:duettoresearch/APEX.git",
        ],
    );
    base
}

/// #9089: an SSH alias for github.com is the github.com repository, in the
/// scp-style and `ssh://` spellings both.
/// Test: itself.
#[test]
fn an_ssh_alias_origin_matches_its_github_remote() {
    let aliases = duetto_aliases();
    let want = canonical_remote("https://github.com/duettoresearch/apex.git");
    for url in [
        "git@github-duetto:duettoresearch/APEX.git",
        "github-duetto:duettoresearch/apex",
        "ssh://git@github-duetto/duettoresearch/APEX.git",
        "git+ssh://git@github-duetto/duettoresearch/apex",
    ] {
        assert_eq!(
            canonical_remote_with(url, &aliases),
            want,
            "canonicalizing {url}"
        );
    }
}

/// #9089: a host with no alias entry, a non-SSH scheme, and a local path that
/// merely contains the alias text all canonicalize exactly as before.
/// Test: itself.
#[test]
fn a_host_with_no_alias_entry_canonicalizes_as_before() {
    let aliases = duetto_aliases();
    for url in [
        "git@github.com:acme/widget.git",
        "git@gitlab.com:acme/widget.git",
        "git@gh-other:acme/widget.git",
        "ssh://git@gh-other:2222/acme/widget.git",
        "https://github-duetto/acme/widget",
        "/tmp/github-duetto:x/widget",
    ] {
        assert_eq!(
            canonical_remote_with(url, &aliases),
            canonical_remote(url),
            "canonicalizing {url}"
        );
    }
}

/// #9089 end to end through the identity check: a real checkout whose origin
/// is the alias remote is accepted for the github.com clone URL.
/// Test: itself.
#[test]
fn an_ssh_alias_origin_checkout_matches_its_github_remote() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let base = alias_origin_checkout(tmp.path());

    verify_remote_matches(
        &base,
        "https://github.com/duettoresearch/apex.git",
        &duetto_aliases(),
    )
    .expect("an alias origin is the repository its real host serves");
}

/// FAIL-OPEN CHECK, error arm (#9089): an ssh config that is missing, is not
/// a readable file, or holds no parseable `Host` entry resolves no alias, so
/// the alias origin is still `RemoteMismatch` — today's behaviour, never a
/// match.
/// Test: itself.
#[test]
fn an_unreadable_ssh_config_never_turns_an_alias_into_a_match() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let base = alias_origin_checkout(tmp.path());
    let garbage = tmp.path().join("garbage-config");
    std::fs::write(&garbage, "\u{0}\u{1} === not ssh config\nHostName\n").expect("write");

    for config in [
        tmp.path().join("no-such-config"),
        tmp.path().to_path_buf(),
        garbage,
    ] {
        let aliases = SshHostAliases::load(&config);
        let err = verify_remote_matches(
            &base,
            "https://github.com/duettoresearch/apex.git",
            &aliases,
        )
        .expect_err("an unread alias must not match");
        assert!(
            matches!(err, ColdStartError::RemoteMismatch { .. }),
            "config {config:?}: expected RemoteMismatch, got {err:?}"
        );
    }
}

/// Why: the host is load-bearing here. `parse_github_path` drops it on
/// purpose, so a comparison built on that parser would call two different
/// hosts' `owner/repo` the same repository — exactly the confusion the remote
/// check exists to catch.
/// Test: itself.
#[test]
fn different_hosts_do_not_match() {
    assert_ne!(
        canonical_remote("git@github.com:acme/widget.git"),
        canonical_remote("git@gitlab.com:acme/widget.git")
    );
    assert_ne!(
        canonical_remote("https://github.com/acme/widget"),
        canonical_remote("https://github.com/other/widget")
    );
}

/// 🔴 #9124: a refused checkout whose requested URL carries a token names the
/// URL with its userinfo redacted, in both Display and Debug.
/// Test: itself.
#[test]
fn a_remote_mismatch_never_carries_the_requested_token() {
    const TOKEN: &str = "ghp_9124SyntheticNotARealToken0000";
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let origin = init_origin(&tmp.path().join("origin")).to_path_buf();
    let base = tmp.path().join("base");
    clone_to(&origin, &base);

    let requested = format!("https://x-access-token:{TOKEN}@github.com/someone/else.git");
    let err = ensure_managed_checkout_at(&base, &requested, None)
        .expect_err("a different remote must be refused");

    let shown = err.to_string();
    let debug = format!("{err:?}");
    assert!(
        !shown.contains(TOKEN),
        "the error message carries the token"
    );
    assert!(
        !debug.contains(TOKEN),
        "the error's Debug carries the token"
    );
    assert!(
        shown.contains("https://***@github.com/someone/else.git"),
        "the message no longer names the requested repository"
    );
}

/// DECIDED BEHAVIOR: an existing checkout whose origin names a different
/// repository is an error, not an auto-fix.
///
/// Why: no check exists anywhere today — the standalone driver pulls from
/// whatever `origin` happens to be, so re-pointing an alias at a new URL keeps
/// serving the old remote forever under the new name. Refusing is the only
/// non-destructive answer: re-pointing `origin` under a directory that may hold
/// branches and unpushed commits from the OTHER repository is not a repair.
/// Test: itself.
#[test]
fn existing_checkout_on_a_different_remote_fails_loud() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let origin = init_origin(&tmp.path().join("origin")).to_path_buf();
    let base = tmp.path().join("base");
    clone_to(&origin, &base);

    let err = ensure_managed_checkout_at(&base, "https://github.com/someone/else.git", None)
        .expect_err("a different remote must be refused");

    assert!(
        matches!(err, ColdStartError::RemoteMismatch { .. }),
        "expected RemoteMismatch, got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains(&base.display().to_string()),
        "names path: {msg}"
    );
    assert!(msg.contains("someone/else"), "names requested repo: {msg}");
    assert!(
        msg.contains("will not re-point"),
        "states the refusal: {msg}"
    );
}

/// FAIL-SAFE: a checkout with no `origin` at all cannot be matched, so it is
/// refused rather than assumed to be the requested repo.
///
/// Test: itself.
#[test]
fn existing_checkout_without_an_origin_fails_loud() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let base = init_origin(&tmp.path().join("base")).to_path_buf();

    let err = ensure_managed_checkout_at(&base, "https://github.com/acme/widget.git", None)
        .expect_err("a checkout with no origin must be refused");
    assert!(
        matches!(err, ColdStartError::NoOrigin { .. }),
        "expected NoOrigin, got {err:?}"
    );
}

/// FAIL-SAFE, and with the RIGHT reason: a checkout whose remote git cannot
/// read is `OriginUnreadable`, not `NoOrigin` (#4734).
///
/// Why: both refuse, so the pre-#4734 collapse was safe here — but it told the
/// operator the checkout "has no remote.origin.url" and to remove the
/// directory, which is wrong advice for a `.git/config` git declined to parse.
/// What: builds a real checkout, then corrupts `.git/config` so
/// `git config --get` exits 128, and asserts both the variant and that the
/// message carries git's own reason.
/// Test: itself.
#[test]
fn existing_checkout_with_an_unreadable_remote_fails_loud() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let base = init_origin(&tmp.path().join("base")).to_path_buf();
    std::fs::write(base.join(".git/config"), "[remote \"origin\"\n").expect("corrupt config");

    let err = ensure_managed_checkout_at(&base, "https://github.com/acme/widget.git", None)
        .expect_err("an unreadable remote must be refused");
    assert!(
        matches!(err, ColdStartError::OriginUnreadable { .. }),
        "expected OriginUnreadable, got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains("could not be read"),
        "states the real reason: {msg}"
    );
}

/// DECIDED BEHAVIOR: a dirty existing checkout WARNS AND PROCEEDS.
///
/// Why: this reverses an earlier fail-loud decision on this same path. A dirty
/// tree cannot be fast-forwarded, but since #4957 the session branch is cut
/// from a freshly-fetched `origin/<default>` and never inherits the base
/// checkout's local `HEAD`, so a skipped fast-forward cannot leak stale content
/// into the session. Refusing would block the common case — the tm checkout is
/// shared with the operator's editors, making uncommitted content its steady
/// state (ADR-0030 §4/§5) — to guard against nothing.
///
/// What must NOT happen is silence, which is the half of the `pull_ff_only`
/// (`core::standalone::load.rs`) shape that really is a defect: it warns where
/// nobody looks and returns `Ok(())`. So this asserts four things at once — the
/// call succeeds, the skip is REPORTED to the caller naming what is dirty, the
/// uncommitted content survives, and the local branch genuinely did not move
/// while `origin/main` genuinely did.
/// Test: itself.
#[test]
fn dirty_existing_checkout_warns_and_proceeds() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let origin = init_origin(&tmp.path().join("origin")).to_path_buf();
    let base = tmp.path().join("base");
    clone_to(&origin, &base);
    let head_before = git(&base, &["rev-parse", "HEAD"]);

    // Origin moves forward, so a fast-forward is genuinely AVAILABLE — without
    // this the "was not fast-forwarded" assertion would pass vacuously.
    std::fs::write(origin.join("file.txt"), "v2 from origin\n").expect("write");
    git(&origin, &["add", "."]);
    git(&origin, &["commit", "-q", "-m", "moved forward"]);

    // The operator's uncommitted work.
    std::fs::write(base.join("file.txt"), "MY UNCOMMITTED EDIT\n").expect("write");

    let url = origin.to_string_lossy().into_owned();
    let checkout = ensure_managed_checkout_at(&base, &url, None)
        .expect("a dirty checkout must NOT be refused");

    assert!(checkout.reused);
    let reason = checkout
        .refresh_skipped
        .as_deref()
        .expect("the skip must be REPORTED to the caller, not merely logged");
    assert!(
        reason.contains("uncommitted changes"),
        "reason must state the cause: {reason}"
    );
    assert!(
        reason.contains("file.txt"),
        "reason must name what is dirty: {reason}"
    );

    // The edit survives, and the local branch did not move.
    assert_eq!(
        std::fs::read_to_string(base.join("file.txt")).expect("read"),
        "MY UNCOMMITTED EDIT\n"
    );
    assert_eq!(
        git(&base, &["rev-parse", "HEAD"]),
        head_before,
        "a dirty tree must not be fast-forwarded"
    );

    // But the FETCH still happened, which is what keeps the session's start
    // point current — the whole reason the skipped fast-forward costs nothing.
    assert_ne!(
        git(&base, &["rev-parse", "refs/remotes/origin/main"]),
        head_before,
        "origin/main must be fetched even when the fast-forward is skipped"
    );
}

/// The success case the refusal must not swallow: a clean checkout on the
/// requested remote is reused, fast-forwarded, and reports NO skip.
///
/// Why: a guard that refuses everything is not a guard. This pins the refresh
/// actually happening — origin moves forward and the reused checkout follows it
/// — and that `refresh_skipped` is `None`, so the notice cannot fire spuriously
/// on every clean run.
/// Test: itself.
#[test]
fn clean_matching_checkout_is_reused_and_refreshed() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let origin = init_origin(&tmp.path().join("origin")).to_path_buf();
    let base = tmp.path().join("base");
    clone_to(&origin, &base);

    // Origin moves forward after the clone.
    std::fs::write(origin.join("file.txt"), "v2 from origin\n").expect("write");
    git(&origin, &["add", "."]);
    git(&origin, &["commit", "-q", "-m", "moved forward"]);

    let url = origin.to_string_lossy().into_owned();
    let checkout = ensure_managed_checkout_at(&base, &url, None).expect("clean reuse succeeds");

    assert!(checkout.reused, "an existing checkout must report reused");
    assert_eq!(checkout.base_path, base);
    assert_eq!(
        checkout.refresh_skipped, None,
        "a clean checkout must report no skipped refresh"
    );
    assert_eq!(
        std::fs::read_to_string(base.join("file.txt")).expect("read"),
        "v2 from origin\n",
        "the reused checkout must have been fast-forwarded"
    );
}

/// A fresh clone into an absent path succeeds and reports `reused = false`.
///
/// Why: the cold-start case itself — no `.git`, no cwd, nothing on disk. Uses a
/// local source repo so the test never touches the network.
/// Test: itself.
#[test]
fn absent_path_is_cloned_and_reports_not_reused() {
    let tmp = tempfile::TempDir::new().expect("temp dir");
    let origin = init_origin(&tmp.path().join("origin")).to_path_buf();
    let base = tmp.path().join("owner").join("repo");

    let url = origin.to_string_lossy().into_owned();
    let checkout = ensure_managed_checkout_at(&base, &url, None).expect("fresh clone succeeds");

    assert!(!checkout.reused, "a fresh clone must not report reused");
    assert!(base.join(".git").exists(), "clone produced a git checkout");
    assert_eq!(
        std::fs::read_to_string(base.join("file.txt")).expect("read"),
        "v1\n"
    );
}

/// Why: a managed checkout can be thousands of lines dirty, and an
/// untruncated status pushes the remedy off the screen.
/// Test: itself.
#[test]
fn dirty_message_truncates_long_status() {
    let entries: Vec<String> = (0..25).map(|i| format!(" M file{i}.txt")).collect();
    let rendered = summarize_entries(&entries);
    assert!(rendered.contains("file0.txt"));
    assert!(rendered.contains("file9.txt"));
    assert!(!rendered.contains("file10.txt"));
    assert!(rendered.contains("… and 15 more"));

    // Short lists render in full with no summary line.
    let short = summarize_entries(&[" M a.txt".to_string()]);
    assert_eq!(short, " M a.txt");
}