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
//! CLI parse and report-line tests for `tm install --reset-agents`
//! (issue #2504) — kept in their own `_tests.rs`-suffixed file (1500-SLOC
//! test cap) rather than growing `tests_behavior_a_tests.rs`, which sits close to
//! the 500-SLOC production cap despite living in `src/bin/tm/`, following the
//! existing `tests_behavior_a/b/c/d/e` split convention.
//!
//! Why: issue #2504 found that composed agent files deployed before per-file
//! manifest tracking existed can never receive bundle updates — the deployer
//! conservatively (and correctly) treats an untracked target file as
//! potentially user-owned and skips it forever. These tests cover the CLI
//! surface for the fix's three parts: `--reset-agents` argument parsing
//! (bare flag = all agents, comma list = filtered), and the human-readable
//! report lines for both the deploy adoption/warning path and the reset
//! summary. Core reconciliation logic (adoption, backup-before-overwrite) is
//! covered by `crates/trusty-mpm/src/core/agent_deployer.rs` and
//! `crates/trusty-mpm/src/core/agent_reset.rs` unit tests.
//! What: `cli_parses_install_reset_agents_all`,
//! `cli_parses_install_reset_agents_filtered`,
//! `deploy_report_lines_flags_adopted_and_untracked_modified`,
//! `reset_report_lines_summarizes_counts`.
//! Test: `cargo test -p trusty-mpm` runs this file as part of the `tm` binary
//! test suite.
use clap::Parser;
use crate::cli::{Cli, Command};
use crate::commands::install::{deploy_report_lines, reset_report_lines};
#[test]
fn cli_parses_install_reset_agents_all() {
// Issue #2504: `--reset-agents` with no value resets every bundled agent.
let cli = Cli::try_parse_from(["trusty-mpm", "install", "--reset-agents"]).unwrap();
match cli.command.unwrap() {
Command::Install { reset_agents, .. } => {
assert_eq!(reset_agents, Some(vec![]));
}
other => panic!("expected Install, got {other:?}"),
}
}
#[test]
fn cli_parses_install_reset_agents_filtered() {
// A comma-separated value list restricts the reset scope.
let cli =
Cli::try_parse_from(["trusty-mpm", "install", "--reset-agents", "engineer,qa"]).unwrap();
match cli.command.unwrap() {
Command::Install { reset_agents, .. } => {
assert_eq!(
reset_agents,
Some(vec!["engineer".to_string(), "qa".to_string()])
);
}
other => panic!("expected Install, got {other:?}"),
}
}
#[test]
fn deploy_report_lines_flags_adopted_and_untracked_modified() {
// Issue #2504: adopted files get a distinct glyph, untracked-modified
// files that are also skipped get a distinct message, and a single
// trailing summary line points at `--reset-agents` — never one warning
// line per untracked file.
let mut result = trusty_mpm::core::agent_deployer::DeployResult::default();
result.adopted.push("adopted-agent.md".to_string());
result.skipped.push("stale-agent.md".to_string());
result.untracked_modified.push("stale-agent.md".to_string());
result.skipped.push("edited-agent.md".to_string());
let dir = tempfile::tempdir().unwrap();
let lines = deploy_report_lines(&result, dir.path());
assert!(
lines
.iter()
.any(|l| l.contains("adopted-agent.md") && l.contains("adopted")),
"lines = {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("stale-agent.md") && l.contains("differs from bundle")),
"lines = {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("edited-agent.md") && l.contains("user-modified")),
"lines = {lines:?}"
);
assert!(
lines
.iter()
.any(|l| l.contains("1 untracked agent file") && l.contains("--reset-agents")),
"lines = {lines:?}"
);
}
#[test]
fn reset_report_lines_summarizes_counts() {
// Issue #2504: `--reset-agents` output must include a scannable total,
// not just per-file lines, since a full reset can touch dozens of files.
let result = trusty_mpm::core::agent_reset::ResetResult {
recomposed: vec!["a.md".to_string(), "b.md".to_string()],
adopted: vec!["c.md".to_string()],
backed_up: vec!["b.md".to_string()],
not_found: vec!["missing-agent".to_string()],
deselected: vec![],
retracted: vec![],
};
let lines = reset_report_lines(&result);
assert!(lines.iter().any(|l| l.contains("a.md (recomposed)")));
assert!(
lines
.iter()
.any(|l| l.contains("b.md (recomposed, backed up)"))
);
assert!(lines.iter().any(|l| l.contains("c.md (adopted)")));
assert!(lines.iter().any(|l| l.contains("missing-agent")));
assert!(
lines.iter().any(|l| l.contains("2 recomposed")
&& l.contains("1 adopted")
&& l.contains("1 backed up")
&& l.contains("1 not found")
&& l.contains("0 deselected")),
"lines = {lines:?}"
);
}
#[test]
fn reset_report_lines_shows_retracted() {
// Issue #4409: the workspace sweep REMOVES shadowing bundled copies rather
// than recomposing them, so its removals need their own reported line —
// otherwise `--reset-agents-workspaces` prints an all-zero summary while
// having deleted files, which reads as a no-op.
let result = trusty_mpm::core::agent_reset::ResetResult {
recomposed: vec![],
adopted: vec![],
backed_up: vec![],
not_found: vec![],
deselected: vec![],
retracted: vec!["engineer.md".to_string(), "qa.md".to_string()],
};
let lines = reset_report_lines(&result);
assert!(
lines
.iter()
.any(|l| l.contains("engineer.md") && l.contains("retracted")),
"lines = {lines:?}"
);
assert!(
lines.iter().any(|l| l.contains("2 retracted")),
"the summary must count retractions: lines = {lines:?}"
);
}
#[test]
fn reset_report_lines_shows_deselected() {
// Issue #2508: a roster-filtered result that DESELECTED a
// manifest-excluded agent must surface it distinctly, not silently.
// (No production caller populates `deselected` since #4409 removed
// `reset_project_agents`; the renderer keeps handling it.)
let result = trusty_mpm::core::agent_reset::ResetResult {
recomposed: vec!["base-agent.md".to_string()],
adopted: vec![],
backed_up: vec![],
not_found: vec![],
deselected: vec!["engineer".to_string()],
retracted: vec![],
};
let lines = reset_report_lines(&result);
assert!(
lines
.iter()
.any(|l| l.contains("engineer") && l.contains("excluded")),
"lines = {lines:?}"
);
assert!(
lines.iter().any(|l| l.contains("1 deselected")),
"lines = {lines:?}"
);
}
#[test]
fn cli_parses_install_reset_agents_workspaces() {
// Issue #2508: `--reset-agents-workspaces` requires `--reset-agents` and
// defaults to false when omitted.
let cli = Cli::try_parse_from([
"trusty-mpm",
"install",
"--reset-agents",
"--reset-agents-workspaces",
])
.unwrap();
match cli.command.unwrap() {
Command::Install {
reset_agents,
reset_agents_workspaces,
..
} => {
assert_eq!(reset_agents, Some(vec![]));
assert!(reset_agents_workspaces);
}
other => panic!("expected Install, got {other:?}"),
}
}
#[test]
fn cli_rejects_reset_agents_workspaces_without_reset_agents() {
// The flag is meaningless (and forbidden) without `--reset-agents`.
let result = Cli::try_parse_from(["trusty-mpm", "install", "--reset-agents-workspaces"]);
assert!(result.is_err());
}