trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
//! Unit tests for [`super`] — bundled agent-source self-heal and auto-deploy
//! (issue #4840).
//!
//! Why: #4840's four load-bearing behaviors are (1) the deploy fires when the
//! compiled-in bundle differs from disk, (2) it is a no-op when identical,
//! (3) it fails open when the target cannot be written, and (4) it WARNS —
//! in a bounded count-plus-preview summary, not one line per file (PR #4848
//! review) — whenever it declines to overwrite something or fails to compose
//! it. Each gets a test here.
//! What: hermetic — every case runs against `tempfile::TempDir`, never the
//! real `~/.trusty-mpm`.
//! Test: this file.

use super::*;
use crate::core::content_source::test_support::rc;

/// The real bundled agent set is materialized into a temp dir per test, so
/// these assertions pin the actual shipped roster rather than a fixture.
fn bundled_agent_basenames() -> Vec<String> {
    roster().iter().map(|(name, _)| name.to_string()).collect()
}

/// The checkout roster (#9011), so no test depends on the cwd or HOME.
fn roster() -> AgentRoster {
    crate::core::content_source::test_support::repo_roster()
}

#[test]
fn agent_bundle_stamp_is_stable_across_calls() {
    // Pure function of the compiled-in table: two calls must agree, or the
    // "cheap when nothing changed" gate would re-materialize on every launch.
    assert_eq!(agent_bundle_stamp(&roster()), agent_bundle_stamp(&roster()));
}

#[test]
fn agent_bundle_stamp_differs_from_skill_stamp() {
    // The agent stamp must fingerprint the `agents/*` slice, not the whole
    // table — otherwise a skill-only change would churn the agent source.
    assert_ne!(
        agent_bundle_stamp(&roster()),
        crate::core::skill_source::skill_bundle_stamp(rc())
    );
}

#[test]
fn materialize_agent_artifacts_writes_all_agents() {
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");

    let written = materialize_agent_artifacts(&agents, &roster()).unwrap();

    let expected = bundled_agent_basenames();
    assert_eq!(written.len(), expected.len());
    for name in expected {
        let content = std::fs::read_to_string(agents.join(&name))
            .unwrap_or_else(|e| panic!("missing {name}: {e}"));
        assert_eq!(
            content,
            roster().require(&name).unwrap(),
            "{name} content must match bundle"
        );
    }
    assert!(
        agents.join("BASE-AGENT.md").exists(),
        "BASE-AGENT.md is the file #4840 measured as stale — it must materialize"
    );
}

#[test]
fn no_deployed_agent_asset_enables_auto_memory() {
    // #7685: Claude Code's subagent `memory:` frontmatter field opts a subagent
    // into its own auto memory. The owner directive is that auto memory is not
    // used in tm sessions — trusty-memory is the memory — so no shipped agent
    // may carry the field. Deploys the real roster and scans what landed, so a
    // future asset that adds the field fails here rather than in a session.
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");

    let written = materialize_agent_artifacts(&agents, &roster()).unwrap();
    assert!(!written.is_empty(), "the roster must not be empty");

    for name in &written {
        let text = std::fs::read_to_string(agents.join(name)).unwrap();
        // The field only means anything inside the leading `---` frontmatter
        // block; prose in the body that happens to start a line with `memory:`
        // is not a setting.
        let Some(rest) = text.strip_prefix("---\n") else {
            continue;
        };
        let Some(frontmatter) = rest.split("\n---").next() else {
            continue;
        };
        for line in frontmatter.lines() {
            assert!(
                !line.trim_start().starts_with("memory:"),
                "{name} sets the subagent `memory:` frontmatter field: {line:?}"
            );
        }
    }
}

#[test]
fn materialize_agent_artifacts_prunes_files_not_in_table() {
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");
    std::fs::create_dir_all(&agents).unwrap();
    // A leftover from a renamed/removed agent the current table no longer lists.
    std::fs::write(agents.join("retired-engineer.md"), "stale\n").unwrap();

    materialize_agent_artifacts(&agents, &roster()).unwrap();

    assert!(!agents.join("retired-engineer.md").exists());
    assert!(agents.join("engineer.md").exists());
}

#[test]
fn ensure_agent_source_fresh_materializes_when_missing() {
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");
    assert!(!agents.exists());

    let refreshed = ensure_agent_source_fresh_with(&agents, &roster()).unwrap();

    assert!(refreshed, "a missing source dir must trigger a refresh");
    assert!(agents.join("BASE-AGENT.md").exists());
    assert!(agents.join(STAMP_FILE_NAME).exists());
}

#[test]
fn ensure_agent_source_fresh_is_noop_when_current() {
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");
    assert!(ensure_agent_source_fresh_with(&agents, &roster()).unwrap());

    let marker = agents.join("BASE-AGENT.md");
    let before = std::fs::metadata(&marker).unwrap().modified().unwrap();

    let refreshed = ensure_agent_source_fresh_with(&agents, &roster()).unwrap();

    assert!(!refreshed, "an already-current source dir must be a no-op");
    assert_eq!(
        before,
        std::fs::metadata(&marker).unwrap().modified().unwrap(),
        "a no-op refresh must not rewrite files"
    );
}

#[test]
fn ensure_agent_source_fresh_prunes_renamed_files() {
    // The #4840 shape after an agent rename: an operator's existing source dir
    // holds a stale stamp (or none) plus a leftover pre-rename file.
    let tmp = tempfile::TempDir::new().unwrap();
    let agents = tmp.path().join("agents");
    std::fs::create_dir_all(&agents).unwrap();
    std::fs::write(agents.join("mpm-old-agent.md"), "stale\n").unwrap();
    std::fs::write(agents.join(STAMP_FILE_NAME), "stale-stamp").unwrap();

    let refreshed = ensure_agent_source_fresh_with(&agents, &roster()).unwrap();

    assert!(refreshed, "a stale stamp must trigger a refresh");
    assert!(!agents.join("mpm-old-agent.md").exists());
    assert!(agents.join("engineer.md").exists());
}

#[test]
fn deploy_summary_lines_is_empty_on_a_clean_deploy() {
    let result = DeployResult {
        deployed: vec!["engineer.md".into()],
        ..DeployResult::default()
    };
    assert!(deploy_summary_lines(&result).is_empty());
}

#[test]
fn deploy_summary_lines_summarises_skips_in_one_line() {
    // PR #4848 review (HIGH): the skipped set can be dozens wide and is never
    // reconciled until someone runs `--reset-agents`, so this runs on every
    // spawn and resume. It must be ONE line with a count + preview, matching
    // `agents::deployer`'s #2504 policy — never one line per file.
    let skipped: Vec<String> = (0..12).map(|i| format!("agent-{i}.md")).collect();
    let result = DeployResult {
        skipped: skipped.clone(),
        untracked_modified: vec!["agent-0.md".into()],
        ..DeployResult::default()
    };

    let lines = deploy_summary_lines(&result);

    assert_eq!(lines.len(), 1, "one summary line, not N: {lines:?}");
    assert!(lines[0].contains("12 agent file(s)"), "{:?}", lines[0]);
    assert!(lines[0].contains("agent-0.md"), "{:?}", lines[0]);
    assert!(lines[0].contains('…'), "elision marker: {:?}", lines[0]);
    assert!(
        !lines[0].contains("agent-11.md"),
        "the preview must be truncated: {:?}",
        lines[0]
    );
    // The actionable pointer survives the shrink — the original silence was
    // half the defect.
    assert!(
        lines[0].contains("--reset-agents agent-0"),
        "{:?}",
        lines[0]
    );
}

#[test]
fn deploy_summary_lines_omits_the_reset_pointer_for_user_owned_edits_only() {
    // PR #4848 review (MEDIUM, round 2): `skipped` mixes two ownership
    // classes — untracked-and-differing files (which `--reset-agents`
    // legitimately resolves) and edited seed-once (user-owned) entries, which
    // it does not. When the only skip is a user-owned edit, the pointer must
    // not appear at all — naming that file would instruct the operator to
    // discard their own deliberate customization.
    let result = DeployResult {
        skipped: vec!["engineer.md".into()],
        ..DeployResult::default()
    };

    let lines = deploy_summary_lines(&result);

    assert_eq!(lines.len(), 1, "{lines:?}");
    assert!(lines[0].contains("engineer.md"), "{:?}", lines[0]);
    assert!(
        !lines[0].contains("--reset-agents"),
        "a user-owned edit must not carry the reset pointer: {:?}",
        lines[0]
    );
}

#[test]
fn deploy_summary_lines_names_the_untracked_modified_file_in_the_pointer() {
    // The companion case: when the skip set mixes a user-owned edit with an
    // untracked-and-differing file, the pointer must name the
    // untracked-modified entry, never `skipped[0]` unconditionally.
    let result = DeployResult {
        skipped: vec!["user-owned-edit.md".into(), "untracked-drift.md".into()],
        untracked_modified: vec!["untracked-drift.md".into()],
        ..DeployResult::default()
    };

    let lines = deploy_summary_lines(&result);

    assert_eq!(lines.len(), 1, "{lines:?}");
    assert!(
        lines[0].contains("--reset-agents untracked-drift"),
        "{:?}",
        lines[0]
    );
    assert!(
        !lines[0].contains("--reset-agents user-owned-edit"),
        "the pointer must never name the user-owned edit: {:?}",
        lines[0]
    );
}

#[test]
fn deploy_summary_lines_reports_failed_agents() {
    // A compose failure means the agent does not land AT ALL — worse than
    // stale, and previously never surfaced anywhere (PR #4848 review, MEDIUM).
    let result = DeployResult {
        failed: vec!["engineer: invalid frontmatter YAML: bad".into()],
        ..DeployResult::default()
    };

    let lines = deploy_summary_lines(&result);

    assert_eq!(lines.len(), 1, "{lines:?}");
    assert!(lines[0].contains("engineer"), "{:?}", lines[0]);
    assert!(
        lines[0].contains("did NOT deploy at all"),
        "the failure must read as worse than stale: {:?}",
        lines[0]
    );
}

#[test]
fn autodeploy_agents_deploys_when_bundle_differs() {
    // The core #4840 fix: nothing on disk, nobody ran `tm install`, and the
    // agents still land.
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    let target = tmp.path().join("claude-config/agents");

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(out.refreshed, "a stale (absent) source must refresh");
    assert!(
        out.warnings.is_empty(),
        "unexpected warnings: {:?}",
        out.warnings
    );
    assert!(
        out.deployed.iter().any(|f| f == "BASE-AGENT.md"),
        "BASE-AGENT.md must deploy: {:?}",
        out.deployed
    );
    assert!(target.join("engineer.md").exists());
}

#[test]
fn autodeploy_agents_is_a_noop_when_already_current() {
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    let target = tmp.path().join("claude-config/agents");
    assert!(
        autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster())).refreshed
    );

    let marker = target.join("engineer.md");
    let before = std::fs::metadata(&marker).unwrap().modified().unwrap();

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(!out.refreshed, "matching checksums must skip the refresh");
    assert!(
        out.deployed.is_empty(),
        "nothing should be rewritten: {:?}",
        out.deployed
    );
    assert!(out.warnings.is_empty(), "{:?}", out.warnings);
    assert_eq!(
        before,
        std::fs::metadata(&marker).unwrap().modified().unwrap(),
        "a no-op run must not rewrite deployed files"
    );
}

#[test]
fn autodeploy_agents_fails_open_when_target_is_unwritable() {
    // A broken deploy must never block a session — it degrades to a warning.
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    // A regular file where the target directory must go: nothing can be
    // written under it, and `create_dir_all` cannot repair it.
    let target = tmp.path().join("not-a-dir");
    std::fs::write(&target, "blocking file\n").unwrap();

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(
        out.refreshed,
        "the source refresh is independent of the target and must still run"
    );
    assert!(out.deployed.is_empty());
    assert!(
        out.warnings.iter().any(|w| w.contains("could not deploy")),
        "the failure must surface as a warning, not an error: {:?}",
        out.warnings
    );
    // The blocking file is untouched — auto-deploy destroys nothing on failure.
    assert_eq!(std::fs::read_to_string(&target).unwrap(), "blocking file\n");
}

#[test]
fn autodeploy_agents_fails_open_when_source_is_unwritable() {
    // The other half of fail-open: the source refresh itself cannot run.
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("not-a-dir");
    std::fs::write(&source, "blocking file\n").unwrap();
    let target = tmp.path().join("claude-config/agents");

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(!out.refreshed);
    assert!(
        out.warnings.iter().any(|w| w.contains("could not refresh")),
        "{:?}",
        out.warnings
    );
}

#[test]
fn autodeploy_agents_warns_when_it_skips_a_user_modified_file() {
    // #4840's second half: the deployer preserves an untracked, differing file
    // — correct, but it used to do so SILENTLY, which is how a three-day-stale
    // BASE-AGENT.md went unnoticed.
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    let target = tmp.path().join("claude-config/agents");
    std::fs::create_dir_all(&target).unwrap();
    std::fs::write(
        target.join("engineer.md"),
        "---\nname: engineer\ndescription: hand-edited\n---\n\nMine.\n",
    )
    .unwrap();

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(
        out.warnings.iter().any(|w| w.contains("engineer.md")),
        "the skipped file must be named in a warning: {:?}",
        out.warnings
    );
    assert!(
        !out.deployed.iter().any(|f| f == "engineer.md"),
        "the user's file must be preserved, not overwritten"
    );
    assert!(
        std::fs::read_to_string(target.join("engineer.md"))
            .unwrap()
            .contains("Mine."),
        "the user's content must survive byte-for-byte"
    );
    // Everything else still deployed — one preserved file does not stall the roster.
    assert!(target.join("BASE-AGENT.md").exists());
}

#[test]
fn autodeploy_agents_for_skips_source_refresh_on_submodule() {
    // When the `agents/agents` git submodule is checked out it is the
    // authoritative, git-tracked source — materializing the compiled-in bundle
    // over it would be destructive. Deploy from it, never rewrite it.
    let tmp = tempfile::TempDir::new().unwrap();
    let checkout = tempfile::TempDir::new().unwrap();
    let submodule = checkout.path().join("agents").join("agents");
    std::fs::create_dir_all(&submodule).unwrap();
    std::fs::write(
        submodule.join("submodule-agent.md"),
        "---\nname: submodule-agent\ndescription: from the submodule\n---\n\nSUBMODULE.\n",
    )
    .unwrap();

    let mut paths = FrameworkPaths::under(tmp.path());
    paths.trusty_mpm_root = Some(checkout.path().to_path_buf());
    assert_eq!(paths.agent_source_dir(), submodule);
    let target = tmp.path().join("claude-config/agents");

    let out = autodeploy_agents_for(&paths, &target, &std::env::temp_dir());

    assert!(
        !out.refreshed,
        "a submodule source is never re-materialized"
    );
    assert!(
        !paths.agents.exists(),
        "must never create the framework source dir when a submodule is in play"
    );
    assert_eq!(
        std::fs::read_dir(&submodule).unwrap().count(),
        1,
        "the git-tracked submodule must be left exactly as found"
    );
    assert!(target.join("submodule-agent.md").exists());
}

#[test]
fn autodeploy_agents_for_falls_back_when_the_submodule_is_empty() {
    // PR #4848 review: an EMPTY `agents/agents` (an uninitialized submodule on
    // a source checkout — and #4840 was originally measured on one) satisfies
    // `agent_source_dir()`'s `is_dir()` test, so the old code deployed from it
    // and landed NOTHING, silently. Fall back to the compiled-in bundle.
    let tmp = tempfile::TempDir::new().unwrap();
    let checkout = tempfile::TempDir::new().unwrap();
    let submodule = checkout.path().join("agents").join("agents");
    std::fs::create_dir_all(&submodule).unwrap();

    let mut paths = FrameworkPaths::under(tmp.path());
    paths.trusty_mpm_root = Some(checkout.path().to_path_buf());
    assert_eq!(paths.agent_source_dir(), submodule);
    let target = tmp.path().join("claude-config/agents");

    let out = autodeploy_agents_for(&paths, &target, &std::env::temp_dir());

    assert!(
        out.refreshed,
        "the empty submodule must not suppress the bundle refresh"
    );
    assert!(
        out.deployed.iter().any(|f| f == "BASE-AGENT.md"),
        "the bundled roster must land: {:?}",
        out.deployed
    );
    assert!(target.join("engineer.md").exists());
    assert_eq!(
        std::fs::read_dir(&submodule).unwrap().count(),
        0,
        "the submodule directory is still never written"
    );
}

#[test]
fn autodeploy_agents_overwrites_a_drifted_bundled_file() {
    // Documented overwrite decision (#4840): a BUNDLED-origin file whose
    // checksum drifted IS re-deployed — the manifest records `origin`, and
    // framework-owned drift is corruption, not user ownership (#4408).
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    let target = tmp.path().join("claude-config/agents");
    assert!(
        !autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()))
            .deployed
            .is_empty()
    );

    // Corrupt a tracked, bundled-origin deployed file.
    std::fs::write(target.join("engineer.md"), "CORRUPTED\n").unwrap();

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), Ok(roster()));

    assert!(
        out.deployed.iter().any(|f| f == "engineer.md"),
        "a drifted bundled-origin file must be re-deployed: {:?}",
        out.deployed
    );
    assert!(
        !std::fs::read_to_string(target.join("engineer.md"))
            .unwrap()
            .contains("CORRUPTED")
    );
    assert!(
        out.warnings.is_empty(),
        "repairing framework-owned drift is not a declined overwrite: {:?}",
        out.warnings
    );
}

/// The roster-unavailable arm (#9011): no content means zero agents. A source
/// dir an earlier roster populated is stale, so nothing deploys from it,
/// nothing refreshes, and no warning line is added (D4: not-installed is the
/// one once-per-process ERROR `report_not_installed` logs).
#[test]
fn autodeploy_agents_deploys_nothing_without_content() {
    use crate::core::content_source::{DevOverride, agent_roster_in};

    let cache = tempfile::TempDir::new().unwrap();
    let missing = || agent_roster_in(cache.path(), DevOverride::Off);
    let tmp = tempfile::TempDir::new().unwrap();
    let source = tmp.path().join("framework/agents");
    let target = tmp.path().join("claude-config/agents");

    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), missing());
    assert!(!out.refreshed);
    assert!(out.deployed.is_empty(), "{:?}", out.deployed);
    assert!(out.warnings.is_empty(), "{:?}", out.warnings);
    assert!(
        !source.exists(),
        "an unavailable roster must not create the source"
    );

    // A previous binary's roster, still on disk.
    materialize_agent_artifacts(&source, &roster()).unwrap();
    let out = autodeploy_agents_with(&source, &target, &std::env::temp_dir(), missing());
    assert!(!out.refreshed);
    assert!(out.deployed.is_empty(), "{:?}", out.deployed);
    assert!(
        !target.join("engineer.md").exists(),
        "a stale source must not deploy"
    );
    assert!(out.warnings.is_empty(), "{:?}", out.warnings);
}