返回 CodeWhale
tests.rs
根目录 / crates / tui / src / skills / system / tests.rs
1 use super::*;
2 use tempfile::TempDir;
3
4 fn skill_file(tmp: &TempDir, name: &str) -> std::path::PathBuf {
5 tmp.path().join(name).join("SKILL.md")
6 }
7
8 fn skill_dir(tmp: &TempDir, name: &str) -> std::path::PathBuf {
9 tmp.path().join(name)
10 }
11
12 fn marker_file(tmp: &TempDir) -> std::path::PathBuf {
13 tmp.path().join(".system-installed-version")
14 }
15
16 // ── fresh install ─────────────────────────────────────────────────────────
17
18 #[test]
19 fn fresh_install_creates_bundled_skills_and_marker() {
20 let tmp = TempDir::new().unwrap();
21 install_system_skills(tmp.path()).unwrap();
22
23 for skill in BUNDLED_SKILLS {
24 assert!(
25 skill_file(&tmp, skill.name).exists(),
26 "{} SKILL.md should be created",
27 skill.name
28 );
29 }
30 assert!(marker_file(&tmp).exists(), "marker should be created");
31
32 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
33 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
34 }
35
36 #[test]
37 fn bundled_integration_skills_use_current_codewhale_commands_and_paths() {
38 for (name, body) in [("mcp-builder", MCP_BUILDER_BODY), ("feishu", FEISHU_BODY)] {
39 assert!(
40 body.contains("codewhale mcp"),
41 "{name} must use the current CLI"
42 );
43 assert!(
44 !body.contains("deepseek mcp"),
45 "{name} must not recommend the retired CLI name"
46 );
47 }
48 assert!(SKILL_CREATOR_BODY.contains("<workspace>/.codewhale/skills"));
49 assert!(SKILL_CREATOR_BODY.contains("~/.codewhale/skills"));
50 assert!(SKILL_INSTALLER_BODY.contains("~/.codewhale/skills"));
51 // Bundled skills must name model-visible tools. `read_file` is retired and
52 // `File`/`Bash` are hidden compatibility names absent from new catalogs.
53 assert!(PDF_BODY.contains("through `bash`"));
54 assert!(HELP_BODY.contains("the `read` tool"));
55 for (name, body) in [
56 ("pdf", PDF_BODY),
57 ("help", HELP_BODY),
58 ("delegate", DELEGATE_BODY),
59 ("best-of-n", BEST_OF_N_BODY),
60 ] {
61 assert!(
62 !body.contains("read_file") && !body.contains("exec_shell"),
63 "{name} must not teach a retired tool name"
64 );
65 assert!(
66 !body.contains("`File`") && !body.contains("`Bash`"),
67 "{name} must not teach the hidden File/Bash tools"
68 );
69 }
70 }
71
72 /// #4227 (requested by @JayBeest): the contributor sync/gate/digest skill
73 /// shipped in generations 8–11 and moved repo-local in generation 12. Its two
74 /// load-bearing refusals — never move a contributor's HEAD, never touch a
75 /// dirty tree — must survive any later edit to the retained body.
76 #[test]
77 fn contributor_onboarding_is_repo_local_and_keeps_its_refusals() {
78 assert!(
79 !is_bundled_skill_name("contributor-onboarding"),
80 "contributor-onboarding must not ship to every user anymore"
81 );
82
83 let body = contributor_onboarding_body();
84 assert!(body.contains("invocation: explicit-only"));
85 // Read-only by default: sync is proposed, never performed.
86 assert!(body.contains("Do not run `git fetch`, `git pull`, `git rebase`"));
87 assert!(body.contains("Do not stash, discard, reset, or commit a dirty tree"));
88 // The gate is quoted from CI rather than paraphrased, and the digest is
89 // built from files rather than generated.
90 assert!(body.contains("cargo clippy --workspace --all-features --locked"));
91 assert!(body.contains(".github/workflows/ci.yml"));
92 assert!(body.contains("Do not call a model provider"));
93 // Provider neutrality: the dogfood step sends nothing anywhere.
94 assert!(body.contains("./target/release/codewhale exec --help"));
95 assert!(body.contains("Never select a provider for them"));
96 // Contributor credit is part of the skill's own contract.
97 assert!(body.contains("@JayBeest"));
98 }
99
100 /// A generation-11 install carries a bundled `contributor-onboarding` copy.
101 /// Generation 12 stops shipping it but never deletes by name alone: an
102 /// installed copy — shipped or user-edited — survives the upgrade, while a
103 /// fresh generation-12 install never creates one.
104 #[test]
105 fn upgrade_to_generation_12_leaves_contributor_onboarding_in_place() {
106 let tmp = TempDir::new().unwrap();
107 fs::write(marker_file(&tmp), "11").unwrap();
108 let dir = skill_dir(&tmp, "contributor-onboarding");
109 fs::create_dir_all(&dir).unwrap();
110 fs::write(dir.join("SKILL.md"), contributor_onboarding_body()).unwrap();
111
112 install_system_skills(tmp.path()).unwrap();
113
114 assert_eq!(
115 fs::read_to_string(skill_file(&tmp, "contributor-onboarding")).unwrap(),
116 contributor_onboarding_body(),
117 "upgrade must leave the installed copy untouched"
118 );
119 assert_eq!(
120 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
121 BUNDLED_SKILL_VERSION
122 );
123
124 let fresh = TempDir::new().unwrap();
125 install_system_skills(fresh.path()).unwrap();
126 assert!(
127 !skill_file(&fresh, "contributor-onboarding").exists(),
128 "fresh installs must not receive the repo-local skill"
129 );
130 }
131
132 /// Generation 13 trims `social-media` and `health` from the bundle and moves
133 /// `feedback` repo-local. None of the three may ship to new installs; a
134 /// generation-12 `feedback` copy is left in place, never deleted by name.
135 #[test]
136 fn generation_13_trims_pack_and_feedback_goes_repo_local() {
137 for name in ["social-media", "health", "feedback"] {
138 assert!(
139 !is_bundled_skill_name(name),
140 "{name} must not ship in generation 13"
141 );
142 }
143
144 let tmp = TempDir::new().unwrap();
145 fs::write(marker_file(&tmp), "12").unwrap();
146 let dir = skill_dir(&tmp, "feedback");
147 fs::create_dir_all(&dir).unwrap();
148 fs::write(dir.join("SKILL.md"), feedback_body()).unwrap();
149
150 install_system_skills(tmp.path()).unwrap();
151
152 assert_eq!(
153 fs::read_to_string(skill_file(&tmp, "feedback")).unwrap(),
154 feedback_body(),
155 "upgrade must leave the installed feedback copy untouched"
156 );
157
158 let fresh = TempDir::new().unwrap();
159 install_system_skills(fresh.path()).unwrap();
160 for name in ["social-media", "health", "feedback"] {
161 assert!(
162 !skill_file(&fresh, name).exists(),
163 "fresh installs must not receive {name}"
164 );
165 }
166 }
167
168 #[test]
169 fn fresh_install_skills_parse_for_discovery() {
170 let tmp = TempDir::new().unwrap();
171 install_system_skills(tmp.path()).unwrap();
172
173 let registry = crate::skills::SkillRegistry::discover(tmp.path());
174 assert!(
175 registry.warnings().is_empty(),
176 "bundled skills should parse cleanly: {:?}",
177 registry.warnings()
178 );
179
180 for skill in BUNDLED_SKILLS {
181 let parsed = registry
182 .get(skill.name)
183 .unwrap_or_else(|| panic!("{} should be discoverable", skill.name));
184 assert!(
185 !parsed.description.is_empty(),
186 "{} should include model-visible description",
187 skill.name
188 );
189 }
190 }
191
192 #[test]
193 fn corrupt_marker_is_repaired_without_overwriting_user_skill_body() {
194 let tmp = TempDir::new().unwrap();
195 install_system_skills(tmp.path()).unwrap();
196
197 let user_body = "user-edited body";
198 fs::write(skill_file(&tmp, "delegate"), user_body).unwrap();
199 fs::remove_dir_all(skill_dir(&tmp, "skill-creator")).unwrap();
200 fs::write(marker_file(&tmp), "not-a-version").unwrap();
201
202 install_system_skills(tmp.path()).unwrap();
203
204 assert_eq!(
205 fs::read_to_string(skill_file(&tmp, "delegate")).unwrap(),
206 user_body
207 );
208 assert!(skill_file(&tmp, "skill-creator").exists());
209 assert_eq!(
210 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
211 BUNDLED_SKILL_VERSION
212 );
213 }
214
215 #[test]
216 fn directory_marker_is_replaced_and_user_skills_are_preserved() {
217 let tmp = TempDir::new().unwrap();
218 install_system_skills(tmp.path()).unwrap();
219
220 let user_body = "user-edited body";
221 fs::write(skill_file(&tmp, "delegate"), user_body).unwrap();
222 fs::remove_dir_all(skill_dir(&tmp, "skill-creator")).unwrap();
223 fs::remove_file(marker_file(&tmp)).unwrap();
224 fs::create_dir(marker_file(&tmp)).unwrap();
225 fs::write(marker_file(&tmp).join("stale-entry"), "stale").unwrap();
226
227 install_system_skills(tmp.path()).unwrap();
228
229 assert_eq!(
230 fs::read_to_string(skill_file(&tmp, "delegate")).unwrap(),
231 user_body
232 );
233 assert!(skill_file(&tmp, "skill-creator").exists());
234 assert!(marker_file(&tmp).is_file());
235 assert_eq!(
236 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
237 BUNDLED_SKILL_VERSION
238 );
239 }
240
241 #[test]
242 fn invalid_marker_is_repaired_even_when_no_skill_body_changes() {
243 let tmp = TempDir::new().unwrap();
244 install_system_skills(tmp.path()).unwrap();
245 fs::write(marker_file(&tmp), "").unwrap();
246
247 install_system_skills(tmp.path()).unwrap();
248
249 assert_eq!(
250 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
251 BUNDLED_SKILL_VERSION
252 );
253 }
254
255 #[test]
256 fn bundled_catalog_has_two_complete_truthful_tiers() {
257 for skill in BUNDLED_SKILLS {
258 assert!(
259 bundled_skill_tier(skill.name).is_some(),
260 "{} must have a picker tier",
261 skill.name
262 );
263 }
264 assert_eq!(
265 bundled_skill_tier("best-of-n"),
266 Some(BundledSkillTier::CoreAgentic)
267 );
268 assert_eq!(
269 bundled_skill_tier("pdf"),
270 Some(BundledSkillTier::FormatTooling)
271 );
272 assert_eq!(bundled_skill_tier("user-created"), None);
273 assert!(
274 !is_bundled_skill_name("imagine"),
275 "do not advertise image generation without an image-generation tool"
276 );
277 }
278
279 // ── idempotence ───────────────────────────────────────────────────────────
280
281 #[test]
282 fn calling_twice_is_idempotent() {
283 let tmp = TempDir::new().unwrap();
284 install_system_skills(tmp.path()).unwrap();
285
286 for skill in BUNDLED_SKILLS {
287 fs::write(
288 skill_file(&tmp, skill.name),
289 format!("{}-sentinel", skill.name),
290 )
291 .unwrap();
292 }
293
294 install_system_skills(tmp.path()).unwrap();
295
296 for skill in BUNDLED_SKILLS {
297 let body = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
298 assert_eq!(
299 body,
300 format!("{}-sentinel", skill.name),
301 "second install should not overwrite {}",
302 skill.name
303 );
304 }
305 }
306
307 // ── user deleted a directory ──────────────────────────────────────────────
308
309 #[test]
310 fn user_deleted_dir_is_not_recreated() {
311 let tmp = TempDir::new().unwrap();
312 install_system_skills(tmp.path()).unwrap();
313
314 // Simulate user deliberately removing one skill directory.
315 fs::remove_dir_all(skill_dir(&tmp, "delegate")).unwrap();
316
317 // Re-launch must NOT recreate the deleted directory.
318 install_system_skills(tmp.path()).unwrap();
319
320 assert!(
321 !skill_file(&tmp, "delegate").exists(),
322 "delegate must not be recreated after user deleted it"
323 );
324 assert!(
325 skill_file(&tmp, "skill-creator").exists(),
326 "skill-creator should still be present (not deleted by user)"
327 );
328 }
329
330 #[test]
331 fn user_deleted_all_dirs_are_not_recreated() {
332 let tmp = TempDir::new().unwrap();
333 install_system_skills(tmp.path()).unwrap();
334
335 for skill in BUNDLED_SKILLS {
336 fs::remove_dir_all(skill_dir(&tmp, skill.name)).unwrap();
337 }
338
339 install_system_skills(tmp.path()).unwrap();
340
341 for skill in BUNDLED_SKILLS {
342 assert!(
343 !skill_file(&tmp, skill.name).exists(),
344 "{} must not be recreated after user deletion",
345 skill.name
346 );
347 }
348 }
349
350 // ── version bump re-installs ──────────────────────────────────────────────
351
352 #[test]
353 fn outdated_marker_triggers_reinstall_of_existing_skills() {
354 let tmp = TempDir::new().unwrap();
355 // Exact shipped bodies present with old marker: refresh is allowed and
356 // newer skills are added. Non-matching user content is preserved
357 // elsewhere (see upgrade_preserves_user_modified_bundled_skill_body).
358 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
359 fs::create_dir_all(skill_dir(&tmp, skill.name)).unwrap();
360 fs::write(skill_file(&tmp, skill.name), skill.body).unwrap();
361 }
362 fs::write(marker_file(&tmp), "0").unwrap();
363
364 install_system_skills(tmp.path()).unwrap();
365
366 for skill in BUNDLED_SKILLS {
367 assert!(
368 skill_file(&tmp, skill.name).exists(),
369 "{} should be installed after marker upgrade",
370 skill.name
371 );
372 let content = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
373 assert_eq!(
374 content, skill.body,
375 "{} body should match shipped",
376 skill.name
377 );
378 }
379 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
380 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
381 }
382
383 // ── partial previous install ─────────────────────────────────────────────
384
385 #[test]
386 fn version_bump_adds_skills_introduced_after_marker() {
387 let tmp = TempDir::new().unwrap();
388 // Pre-v5 install: only skills introduced through v4, with exact bodies.
389 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
390 fs::create_dir_all(skill_dir(&tmp, skill.name)).unwrap();
391 fs::write(skill_file(&tmp, skill.name), skill.body).unwrap();
392 }
393 fs::write(marker_file(&tmp), "4").unwrap();
394
395 install_system_skills(tmp.path()).unwrap();
396
397 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in == 5) {
398 assert!(
399 skill_file(&tmp, skill.name).exists(),
400 "v5 skill {} should be installed on upgrade",
401 skill.name
402 );
403 }
404 // Unchanged exact bodies remain current.
405 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
406 let content = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
407 assert_eq!(content, skill.body);
408 }
409 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
410 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
411 }
412
413 #[test]
414 fn version_bump_from_v8_adds_handoff_without_recreating_deleted_skills() {
415 let tmp = TempDir::new().unwrap();
416 fs::write(marker_file(&tmp), "8").unwrap();
417
418 install_system_skills(tmp.path()).unwrap();
419
420 assert!(skill_file(&tmp, "handoff").is_file());
421 assert!(
422 !skill_file(&tmp, "delegate").exists(),
423 "an intentionally absent older skill must stay absent"
424 );
425 assert_eq!(
426 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
427 BUNDLED_SKILL_VERSION
428 );
429 }
430
431 #[test]
432 fn version_bump_from_v5_adds_best_of_n_without_recreating_deleted_skills() {
433 let tmp = TempDir::new().unwrap();
434 fs::write(marker_file(&tmp), "5").unwrap();
435
436 install_system_skills(tmp.path()).unwrap();
437
438 assert!(skill_file(&tmp, "best-of-n").is_file());
439 assert!(
440 !skill_file(&tmp, "delegate").exists(),
441 "an intentionally absent older skill must stay absent"
442 );
443 assert_eq!(
444 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
445 BUNDLED_SKILL_VERSION
446 );
447 }
448
449 #[test]
450 fn version_bump_respects_deleted_existing_skill_while_adding_new_skill() {
451 let tmp = TempDir::new().unwrap();
452
453 // Simulate v2 where older bundled skills had been deliberately removed
454 // before later versions introduced more system skills.
455 fs::write(marker_file(&tmp), "2").unwrap();
456
457 install_system_skills(tmp.path()).unwrap();
458
459 assert!(
460 !skill_file(&tmp, "skill-creator").exists(),
461 "version bump should not recreate deleted skill-creator"
462 );
463 assert!(
464 !skill_file(&tmp, "delegate").exists(),
465 "version bump should not recreate deleted delegate"
466 );
467 for skill in BUNDLED_SKILLS
468 .iter()
469 .filter(|skill| skill.introduced_in > 2)
470 {
471 assert!(
472 skill_file(&tmp, skill.name).exists(),
473 "version bump should install newly introduced {}",
474 skill.name
475 );
476 }
477 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
478 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
479 }
480
481 // ── upgrade ───────────────────────────────────────────────────────────────
482
483 #[test]
484 fn upgrade_from_v4_installs_pack_and_retires_unchanged_v4_best_practices() {
485 let tmp = TempDir::new().unwrap();
486 // Simulate a v4 install: marker + legacy skill bodies.
487 fs::create_dir_all(skill_dir(&tmp, "v4-best-practices")).unwrap();
488 fs::write(
489 skill_file(&tmp, "v4-best-practices"),
490 V4_BEST_PRACTICES_BODY,
491 )
492 .unwrap();
493 fs::write(marker_file(&tmp), "4").unwrap();
494
495 install_system_skills(tmp.path()).unwrap();
496
497 assert!(
498 !skill_dir(&tmp, "v4-best-practices").exists(),
499 "unchanged v4-best-practices must be retired"
500 );
501 assert!(skill_file(&tmp, "debug").exists());
502 assert!(skill_file(&tmp, "docx").exists());
503 assert!(skill_file(&tmp, "release").exists());
504 // Feishu is optional — not auto-installed by the default pack.
505 assert!(
506 !skill_dir(&tmp, "feishu").exists(),
507 "feishu must not be universally installed"
508 );
509 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
510 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
511 }
512
513 #[test]
514 fn upgrade_preserves_modified_v4_best_practices() {
515 let tmp = TempDir::new().unwrap();
516 fs::create_dir_all(skill_dir(&tmp, "v4-best-practices")).unwrap();
517 fs::write(
518 skill_file(&tmp, "v4-best-practices"),
519 "---\nname: v4-best-practices\ndescription: user-owned\n---\n\n# mine\n",
520 )
521 .unwrap();
522 fs::write(marker_file(&tmp), "4").unwrap();
523
524 install_system_skills(tmp.path()).unwrap();
525
526 assert!(skill_dir(&tmp, "v4-best-practices").exists());
527 let body = fs::read_to_string(skill_file(&tmp, "v4-best-practices")).unwrap();
528 assert!(
529 body.contains("user-owned"),
530 "modified body must be preserved"
531 );
532 }
533
534 #[test]
535 fn upgrade_preserves_user_modified_bundled_skill_body() {
536 let tmp = TempDir::new().unwrap();
537 install_system_skills(tmp.path()).unwrap();
538 let path = skill_file(&tmp, "debug");
539 fs::write(
540 &path,
541 "---\nname: debug\ndescription: customized\n---\n\n# custom\n",
542 )
543 .unwrap();
544 // Force version bump attempt
545 fs::write(marker_file(&tmp), "4").unwrap();
546 install_system_skills(tmp.path()).unwrap();
547 let body = fs::read_to_string(path).unwrap();
548 assert!(
549 body.contains("customized"),
550 "user edit must not be overwritten by name alone"
551 );
552 }
553
554 /// A generation-10 install carries the Registry-first `mcp-discovery` body.
555 /// The generation-11 rewrite is only real for existing users if that untouched
556 /// copy is actually replaced — the digest allowance is what makes the upgrade
557 /// reach them instead of stopping at "body differs, must be the user's".
558 #[test]
559 fn upgrade_from_generation_10_refreshes_untouched_mcp_discovery() {
560 let tmp = TempDir::new().unwrap();
561 let old = MCP_DISCOVERY_GENERATION_10_BODY;
562 let path = skill_file(&tmp, "mcp-discovery");
563 fs::create_dir_all(skill_dir(&tmp, "mcp-discovery")).unwrap();
564 fs::write(&path, old).unwrap();
565 fs::write(marker_file(&tmp), "10").unwrap();
566
567 install_system_skills(tmp.path()).unwrap();
568
569 assert_eq!(
570 fs::read_to_string(&path).unwrap(),
571 MCP_DISCOVERY_BODY,
572 "an unmodified generation-10 body must upgrade to the shipped body"
573 );
574 assert_eq!(
575 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
576 BUNDLED_SKILL_VERSION
577 );
578 }
579
580 #[test]
581 fn upgrade_from_generation_10_preserves_user_edited_mcp_discovery() {
582 let tmp = TempDir::new().unwrap();
583 let edited = format!("{MCP_DISCOVERY_GENERATION_10_BODY}\n- my own house rule\n");
584 let path = skill_file(&tmp, "mcp-discovery");
585 fs::create_dir_all(skill_dir(&tmp, "mcp-discovery")).unwrap();
586 fs::write(&path, &edited).unwrap();
587 fs::write(marker_file(&tmp), "10").unwrap();
588
589 install_system_skills(tmp.path()).unwrap();
590
591 assert_eq!(
592 fs::read_to_string(&path).unwrap(),
593 edited,
594 "an edited copy stays the user's, even one derived from a shipped body"
595 );
596 }
597
598 #[test]
599 fn upgrade_preserves_an_intentionally_empty_mcp_discovery() {
600 let tmp = TempDir::new().unwrap();
601 let path = skill_file(&tmp, "mcp-discovery");
602 fs::create_dir_all(skill_dir(&tmp, "mcp-discovery")).unwrap();
603 fs::write(&path, "").unwrap();
604 fs::write(marker_file(&tmp), "10").unwrap();
605 install_system_skills(tmp.path()).unwrap();
606 assert_eq!(fs::read_to_string(path).unwrap(), "");
607 }
608
609 #[test]
610 fn mcp_discovery_deleted_at_generation_10_stays_deleted() {
611 let tmp = TempDir::new().unwrap();
612 fs::write(marker_file(&tmp), "10").unwrap();
613
614 install_system_skills(tmp.path()).unwrap();
615
616 assert!(
617 !skill_file(&tmp, "mcp-discovery").exists(),
618 "the digest allowance must not resurrect a skill the user removed"
619 );
620 }
621
622 /// The retained body is evidence, not decoration: it must be the superseded
623 /// text (Registry-first, `registry_sync {}`, always-present tools), never a
624 /// stale copy of the current one, or the allowance would silently do nothing.
625 #[test]
626 fn retained_generation_10_body_is_the_superseded_one() {
627 let old = MCP_DISCOVERY_GENERATION_10_BODY;
628 assert_ne!(old, MCP_DISCOVERY_BODY);
629 assert!(is_superseded_shipped_body("mcp-discovery", old));
630 assert!(!is_superseded_shipped_body(
631 "mcp-discovery",
632 MCP_DISCOVERY_BODY
633 ));
634 assert!(!is_superseded_shipped_body("debug", old));
635 // The stale generation-10 facts the rewrite exists to remove.
636 assert!(old.contains("registry_sync {}"));
637 assert!(old.contains("available in the active tool"));
638 assert!(MCP_DISCOVERY_BODY.contains("registry_sync {query:"));
639 }
640
641 #[test]
642 fn end_user_pack_skills_parse_for_discovery() {
643 let tmp = TempDir::new().unwrap();
644 install_system_skills(tmp.path()).unwrap();
645 let registry = crate::skills::SkillRegistry::discover(tmp.path());
646 assert!(
647 registry.warnings().is_empty(),
648 "bundled skills should parse cleanly: {:?}",
649 registry.warnings()
650 );
651 for name in [
652 "debug", "test", "review", "document", "docx", "release", "plan", "verify",
653 ] {
654 assert!(registry.get(name).is_some(), "{name} must be discoverable");
655 }
656 }
657
658 #[test]
659 fn procedural_skill_homes_remain_bundled_and_lazy() {
660 for name in ["debug", "best-of-n", "simplify", "verify", "test", "review"] {
661 assert!(
662 is_bundled_skill_name(name),
663 "procedural skill home must remain available on demand: {name}"
664 );
665 }
666 }
667
668 #[test]
669 fn generation_14_refreshes_known_bodies_and_preserves_customizations_and_deletions() {
670 for (name, old) in SUPERSEDED_BODIES
671 .iter()
672 .filter(|(name, _)| *name != "mcp-discovery")
673 {
674 let skill = BUNDLED_SKILLS
675 .iter()
676 .find(|skill| skill.name == *name)
677 .unwrap();
678 assert_ne!(*old, skill.body);
679 for customized in [false, true] {
680 let tmp = TempDir::new().unwrap();
681 let file = skill_file(&tmp, name);
682 fs::create_dir_all(skill_dir(&tmp, name)).unwrap();
683 let body = if customized {
684 format!("{old}\nMy instructions.\n")
685 } else {
686 old.to_string()
687 };
688 fs::write(&file, &body).unwrap();
689 fs::write(marker_file(&tmp), "13").unwrap();
690 install_system_skills(tmp.path()).unwrap();
691 assert_eq!(
692 fs::read_to_string(file).unwrap(),
693 if customized {
694 body
695 } else {
696 skill.body.to_string()
697 },
698 "{name}"
699 );
700 }
701 let tmp = TempDir::new().unwrap();
702 fs::write(marker_file(&tmp), "13").unwrap();
703 install_system_skills(tmp.path()).unwrap();
704 assert!(!skill_file(&tmp, name).exists(), "{name} must stay deleted");
705 }
706 }
707
708 #[test]
709 fn generation_15_refreshes_help_and_pdf_from_generation_14() {
710 for name in ["help", "pdf"] {
711 let old = SUPERSEDED_BODIES
712 .iter()
713 .find(|(entry, _)| *entry == name)
714 .map(|(_, body)| *body)
715 .expect("generation-14 body retained");
716 assert!(
717 old.contains("`File`"),
718 "{name} retained body is the old one"
719 );
720 let skill = BUNDLED_SKILLS
721 .iter()
722 .find(|skill| skill.name == name)
723 .unwrap();
724 let tmp = TempDir::new().unwrap();
725 fs::create_dir_all(skill_dir(&tmp, name)).unwrap();
726 fs::write(skill_file(&tmp, name), old).unwrap();
727 fs::write(marker_file(&tmp), "14").unwrap();
728 install_system_skills(tmp.path()).unwrap();
729 assert_eq!(
730 fs::read_to_string(skill_file(&tmp, name)).unwrap(),
731 skill.body,
732 "{name}"
733 );
734 }
735 }
736
737 #[test]
738 fn generation_17_refreshes_previous_plugin_creator_bodies() {
739 let skill = BUNDLED_SKILLS
740 .iter()
741 .find(|skill| skill.name == "plugin-creator")
742 .unwrap();
743 assert!(skill.body.contains("Create `plugin.json`"));
744 assert!(skill.body.contains("## Experimental host code"));
745 for (generation, old) in [
746 (
747 "15",
748 include_str!("../../../assets/skills/plugin-creator/SKILL.generation-15.md"),
749 ),
750 (
751 "16",
752 include_str!("../../../assets/skills/plugin-creator/SKILL.generation-16.md"),
753 ),
754 ] {
755 assert!(is_superseded_shipped_body("plugin-creator", old));
756 assert!(!old.contains("## Experimental host code"));
757 for customized in [false, true] {
758 let tmp = TempDir::new().unwrap();
759 fs::create_dir_all(skill_dir(&tmp, "plugin-creator")).unwrap();
760 let body = if customized {
761 format!("{old}\nMy instructions.\n")
762 } else {
763 old.to_string()
764 };
765 fs::write(skill_file(&tmp, "plugin-creator"), &body).unwrap();
766 fs::write(marker_file(&tmp), generation).unwrap();
767 install_system_skills(tmp.path()).unwrap();
768 assert_eq!(
769 fs::read_to_string(skill_file(&tmp, "plugin-creator")).unwrap(),
770 if customized {
771 body
772 } else {
773 skill.body.to_string()
774 },
775 );
776 }
777 }
778 }
779
779 lines RUST