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
//! `tm install`'s skill-deploy behavior test, split out of `tests_behavior_a_tests.rs`
//! (#7102).
//!
//! Why: `tests_behavior_a_tests.rs` sat exactly at the 500-SLOC production cap, and
//! #7102 adds an assertion to `install_then_deploy_deploys_skills` — that a
//! bundled skill must NOT land in the operator's `~/.claude/skills`. That file's
//! own comments already route each deep skill assertion into a `_tests.rs`
//! sibling for this reason, so the whole test moves here rather than losing the
//! coverage that pays for the cap.
//! What: `install_then_deploy_deploys_skills`, verbatim from its old home plus
//! the #7102 assertion. It exercises
//! `core::skill_install_tiers::deploy_install_skill_tiers` — the function
//! `commands::install::install` calls — so a change to where the installer
//! writes fails here rather than in production.
//! Test: this file IS the test.
use crate::commands::install::{install_to_with, skill_report_lines, test_content, test_roster};
#[test]
fn install_then_deploy_deploys_skills() {
// Regression for #386: a fresh install must populate the skill tier from
// the bundled skill sources, not leave it empty. Calls the SAME function
// `install()`'s skill step actually calls (PR #2818 review, round 3;
// #7102 moved the destination split behind
// `skill_install_tiers::deploy_install_skill_tiers`).
let dir = tempfile::tempdir().unwrap();
let paths = trusty_mpm::core::paths::FrameworkPaths::under(dir.path());
install_to_with(&paths, false, &test_roster(), &test_content()).unwrap();
let result = trusty_mpm::core::skill_install_tiers::deploy_install_skill_tiers(&paths)
.unwrap()
.stats;
// The full /tm- skill portfolio deploys on first install: 21 skills total
// — 19 /tm- portfolio skills (tm-circuit-breaker, tm-verification-protocols,
// tm-tool-usage-guide, tm-git-file-tracking, tm-adr, tm-workflow,
// tm-agent-architecture, tm-postmortem, tm-bug-reporting,
// tm-teaching-templates, tm-ticketing,
// tm-delegation-patterns, tm-session-management, tm-session-pause,
// tm-session-resume, tm-init, tm-issues-prune, tm-cli-operations,
// tm-slack) + tm-doctor + the tm overview skill
// (tm-skills-portfolio epic: the `example-skill.md` placeholder and the
// 11 mpm-* guidance skills no longer ship; the previously-orphaned
// tm-doctor.md is now wired in; issue #2185 added tm-issues-prune; issue
// #2321 added tm-cli-operations; issue #4447 added
// tm-slack-canvas-delivery, replaced 1-for-1 by tm-slack, issue #4761) + 2 (issue
// #2890: code-review-standards, contract-driven-testing — code-critic's
// declared `skills:` dependencies; see
// `tests_behavior_2890_skills_tests.rs` for the dedicated deep assertions
// on those two) + 93 (issue #2903,
// skill-port batch 1: 25 upstream universal/ skill entry points + 68
// references/*.md files carried alongside multi-file skills; see
// `tests_behavior_2903_skills_tests.rs` for the dedicated deep
// assertions) + 7
// (issue #2911: the `documentation-style` bundled skill — entry SKILL.md
// plus 6 references/*.md files; see
// `tests_behavior_2911_documentation_style_tests.rs` for the dedicated
// deep assertions)
// + 7 (issue #2913: the `tm-capabilities` auto-generated harness catalog
// — entry SKILL.md plus 5 generated + 1 hand-authored references/*.md
// files; see `tests_behavior_generate_tests.rs` for the generator's own
// coverage) + 1
// (rust-build-performance, per Bob directive 2026-07-17: a single
// flat-file bundled skill declared by rust-engineer and tauri-engineer;
// see `tests_behavior_rust_build_performance_tests.rs` for the dedicated
// deploy-reachability assertion).
// Issue #5202 retired tm-pr-workflow into tm-workflow: 22 - 1 = 21.
// 21 + 2 + 93 + 7 + 7 + 1 = 131. See `bundle_tm_skills.rs`/
// `bundle_tm_capabilities.rs`/`bundle.rs`
// (CODE_REVIEW_STANDARDS, CONTRACT_DRIVEN_TESTING)/`bundle_all.rs::ALL`
// for the authoritative list.
// Stats report stems (no .md suffix) because each skill lands as
// <dest>/<name>/SKILL.md to match Claude Code's native discovery format.
for expected in [
"tm-circuit-breaker",
"tm-doctor",
"tm-issues-prune",
"tm-cli-operations",
"tm-capabilities",
"documentation-style",
] {
assert!(
result.deployed.contains(&expected.to_string()),
"{expected} must be deployed; got {:?}",
result.deployed
);
}
assert_eq!(
result.deployed.len(),
144,
"expected 144 skill files deployed (19 /tm- portfolio + tm-prose-style (#7423) \
+ 2 references/README.md for tm-ticketing and tm-workflow (#7727) \
+ tm-secrets (#7527) + tm-doctor + tm overview + code-review-standards + \
contract-driven-testing + 93 skill-port batch-1 entries + 7 documentation-style \
entries + 8 tm-capabilities entries (#4946 added references/framework.md) + 1 \
rust-build-performance entry + 1 self-improvement-loop entry (#7723) \
+ 1 rust-delivery-workflow entry (#8192) \
+ 5 tm-epic entries: SKILL.md + 4 references (#8376) \
+ 1 tm-architect-setup entry (#8436)); got {:?}",
result.deployed
);
assert!(result.skipped.is_empty());
assert!(result.unchanged.is_empty());
// Each skill must be deployed as a directory with SKILL.md inside — in the
// MANAGED tier, never the operator's `~/.claude/skills` (#6586, #7102).
assert!(
!paths
.claude_skills_dir()
.join("tm-circuit-breaker")
.exists(),
"a bundled skill must not land in ~/.claude/skills — `tm doctor`'s \
legacy_sources check flags exactly that (#7102)"
);
let deployed = paths
.skill_deploy_dir()
.join("tm-circuit-breaker")
.join("SKILL.md");
assert!(
deployed.is_file(),
"expected skill at {}",
deployed.display()
);
let lines = skill_report_lines(&result);
assert!(
lines.iter().any(|l| l.contains("tm-circuit-breaker")),
"lines = {lines:?}"
);
}