返回 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 live tools. `read_file` is retired and cannot
52 // dispatch (crates/tui/src/tools/registry.rs:2067).
53 assert!(PDF_BODY.contains("built-in `File` tool (`action: \"read\"`)"));
54 for (name, body) in [
55 ("pdf", PDF_BODY),
56 ("help", HELP_BODY),
57 ("delegate", DELEGATE_BODY),
58 ("best-of-n", BEST_OF_N_BODY),
59 ] {
60 assert!(
61 !body.contains("read_file") && !body.contains("exec_shell"),
62 "{name} must not teach a retired tool name"
63 );
64 }
65 }
66
67 /// #4227 (requested by @JayBeest): the contributor sync/gate/digest skill
68 /// ships in generation 8, and its two load-bearing refusals — never move a
69 /// contributor's HEAD, never touch a dirty tree — must survive any later
70 /// edit to the body.
71 #[test]
72 fn contributor_onboarding_ships_at_generation_8_and_keeps_its_refusals() {
73 let skill = BUNDLED_SKILLS
74 .iter()
75 .find(|skill| skill.name == "contributor-onboarding")
76 .expect("contributor-onboarding must be bundled");
77 assert_eq!(skill.introduced_in, 8);
78 // The pin tracks the current catalog generation: 9 added handoff,
79 // 10 added mcp-discovery (#5238).
80 assert_eq!(BUNDLED_SKILL_VERSION, "10");
81
82 let body = skill.body;
83 assert!(body.contains("invocation: explicit-only"));
84 // Read-only by default: sync is proposed, never performed.
85 assert!(body.contains("Do not run `git fetch`, `git pull`, `git rebase`"));
86 assert!(body.contains("Do not stash, discard, reset, or commit a dirty tree"));
87 // The gate is quoted from CI rather than paraphrased, and the digest is
88 // built from files rather than generated.
89 assert!(body.contains("cargo clippy --workspace --all-features --locked"));
90 assert!(body.contains(".github/workflows/ci.yml"));
91 assert!(body.contains("Do not call a model provider"));
92 // Provider neutrality: the dogfood step sends nothing anywhere.
93 assert!(body.contains("./target/release/codewhale exec --help"));
94 assert!(body.contains("Never select a provider for them"));
95 // Contributor credit is part of the skill's own contract.
96 assert!(body.contains("@JayBeest"));
97 }
98
99 #[test]
100 fn fresh_install_skills_parse_for_discovery() {
101 let tmp = TempDir::new().unwrap();
102 install_system_skills(tmp.path()).unwrap();
103
104 let registry = crate::skills::SkillRegistry::discover(tmp.path());
105 assert!(
106 registry.warnings().is_empty(),
107 "bundled skills should parse cleanly: {:?}",
108 registry.warnings()
109 );
110
111 for skill in BUNDLED_SKILLS {
112 let parsed = registry
113 .get(skill.name)
114 .unwrap_or_else(|| panic!("{} should be discoverable", skill.name));
115 assert!(
116 !parsed.description.is_empty(),
117 "{} should include model-visible description",
118 skill.name
119 );
120 }
121 }
122
123 #[test]
124 fn corrupt_marker_is_repaired_without_overwriting_user_skill_body() {
125 let tmp = TempDir::new().unwrap();
126 install_system_skills(tmp.path()).unwrap();
127
128 let user_body = "user-edited body";
129 fs::write(skill_file(&tmp, "delegate"), user_body).unwrap();
130 fs::remove_dir_all(skill_dir(&tmp, "skill-creator")).unwrap();
131 fs::write(marker_file(&tmp), "not-a-version").unwrap();
132
133 install_system_skills(tmp.path()).unwrap();
134
135 assert_eq!(
136 fs::read_to_string(skill_file(&tmp, "delegate")).unwrap(),
137 user_body
138 );
139 assert!(skill_file(&tmp, "skill-creator").exists());
140 assert_eq!(
141 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
142 BUNDLED_SKILL_VERSION
143 );
144 }
145
146 #[test]
147 fn directory_marker_is_replaced_and_user_skills_are_preserved() {
148 let tmp = TempDir::new().unwrap();
149 install_system_skills(tmp.path()).unwrap();
150
151 let user_body = "user-edited body";
152 fs::write(skill_file(&tmp, "delegate"), user_body).unwrap();
153 fs::remove_dir_all(skill_dir(&tmp, "skill-creator")).unwrap();
154 fs::remove_file(marker_file(&tmp)).unwrap();
155 fs::create_dir(marker_file(&tmp)).unwrap();
156 fs::write(marker_file(&tmp).join("stale-entry"), "stale").unwrap();
157
158 install_system_skills(tmp.path()).unwrap();
159
160 assert_eq!(
161 fs::read_to_string(skill_file(&tmp, "delegate")).unwrap(),
162 user_body
163 );
164 assert!(skill_file(&tmp, "skill-creator").exists());
165 assert!(marker_file(&tmp).is_file());
166 assert_eq!(
167 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
168 BUNDLED_SKILL_VERSION
169 );
170 }
171
172 #[test]
173 fn invalid_marker_is_repaired_even_when_no_skill_body_changes() {
174 let tmp = TempDir::new().unwrap();
175 install_system_skills(tmp.path()).unwrap();
176 fs::write(marker_file(&tmp), "").unwrap();
177
178 install_system_skills(tmp.path()).unwrap();
179
180 assert_eq!(
181 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
182 BUNDLED_SKILL_VERSION
183 );
184 }
185
186 #[test]
187 fn bundled_catalog_has_two_complete_truthful_tiers() {
188 for skill in BUNDLED_SKILLS {
189 assert!(
190 bundled_skill_tier(skill.name).is_some(),
191 "{} must have a picker tier",
192 skill.name
193 );
194 }
195 assert_eq!(
196 bundled_skill_tier("best-of-n"),
197 Some(BundledSkillTier::CoreAgentic)
198 );
199 assert_eq!(
200 bundled_skill_tier("pdf"),
201 Some(BundledSkillTier::FormatTooling)
202 );
203 assert_eq!(bundled_skill_tier("user-created"), None);
204 assert!(
205 !is_bundled_skill_name("imagine"),
206 "do not advertise image generation without an image-generation tool"
207 );
208 }
209
210 // ── idempotence ───────────────────────────────────────────────────────────
211
212 #[test]
213 fn calling_twice_is_idempotent() {
214 let tmp = TempDir::new().unwrap();
215 install_system_skills(tmp.path()).unwrap();
216
217 for skill in BUNDLED_SKILLS {
218 fs::write(
219 skill_file(&tmp, skill.name),
220 format!("{}-sentinel", skill.name),
221 )
222 .unwrap();
223 }
224
225 install_system_skills(tmp.path()).unwrap();
226
227 for skill in BUNDLED_SKILLS {
228 let body = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
229 assert_eq!(
230 body,
231 format!("{}-sentinel", skill.name),
232 "second install should not overwrite {}",
233 skill.name
234 );
235 }
236 }
237
238 // ── user deleted a directory ──────────────────────────────────────────────
239
240 #[test]
241 fn user_deleted_dir_is_not_recreated() {
242 let tmp = TempDir::new().unwrap();
243 install_system_skills(tmp.path()).unwrap();
244
245 // Simulate user deliberately removing one skill directory.
246 fs::remove_dir_all(skill_dir(&tmp, "delegate")).unwrap();
247
248 // Re-launch must NOT recreate the deleted directory.
249 install_system_skills(tmp.path()).unwrap();
250
251 assert!(
252 !skill_file(&tmp, "delegate").exists(),
253 "delegate must not be recreated after user deleted it"
254 );
255 assert!(
256 skill_file(&tmp, "skill-creator").exists(),
257 "skill-creator should still be present (not deleted by user)"
258 );
259 }
260
261 #[test]
262 fn user_deleted_all_dirs_are_not_recreated() {
263 let tmp = TempDir::new().unwrap();
264 install_system_skills(tmp.path()).unwrap();
265
266 for skill in BUNDLED_SKILLS {
267 fs::remove_dir_all(skill_dir(&tmp, skill.name)).unwrap();
268 }
269
270 install_system_skills(tmp.path()).unwrap();
271
272 for skill in BUNDLED_SKILLS {
273 assert!(
274 !skill_file(&tmp, skill.name).exists(),
275 "{} must not be recreated after user deletion",
276 skill.name
277 );
278 }
279 }
280
281 // ── version bump re-installs ──────────────────────────────────────────────
282
283 #[test]
284 fn outdated_marker_triggers_reinstall_of_existing_skills() {
285 let tmp = TempDir::new().unwrap();
286 // Exact shipped bodies present with old marker: refresh is allowed and
287 // newer skills are added. Non-matching user content is preserved
288 // elsewhere (see upgrade_preserves_user_modified_bundled_skill_body).
289 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
290 fs::create_dir_all(skill_dir(&tmp, skill.name)).unwrap();
291 fs::write(skill_file(&tmp, skill.name), skill.body).unwrap();
292 }
293 fs::write(marker_file(&tmp), "0").unwrap();
294
295 install_system_skills(tmp.path()).unwrap();
296
297 for skill in BUNDLED_SKILLS {
298 assert!(
299 skill_file(&tmp, skill.name).exists(),
300 "{} should be installed after marker upgrade",
301 skill.name
302 );
303 let content = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
304 assert_eq!(
305 content, skill.body,
306 "{} body should match shipped",
307 skill.name
308 );
309 }
310 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
311 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
312 }
313
314 // ── partial previous install ─────────────────────────────────────────────
315
316 #[test]
317 fn version_bump_adds_skills_introduced_after_marker() {
318 let tmp = TempDir::new().unwrap();
319 // Pre-v5 install: only skills introduced through v4, with exact bodies.
320 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
321 fs::create_dir_all(skill_dir(&tmp, skill.name)).unwrap();
322 fs::write(skill_file(&tmp, skill.name), skill.body).unwrap();
323 }
324 fs::write(marker_file(&tmp), "4").unwrap();
325
326 install_system_skills(tmp.path()).unwrap();
327
328 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in == 5) {
329 assert!(
330 skill_file(&tmp, skill.name).exists(),
331 "v5 skill {} should be installed on upgrade",
332 skill.name
333 );
334 }
335 // Unchanged exact bodies remain current.
336 for skill in BUNDLED_SKILLS.iter().filter(|s| s.introduced_in <= 4) {
337 let content = fs::read_to_string(skill_file(&tmp, skill.name)).unwrap();
338 assert_eq!(content, skill.body);
339 }
340 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
341 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
342 }
343
344 #[test]
345 fn version_bump_from_v8_adds_handoff_without_recreating_deleted_skills() {
346 let tmp = TempDir::new().unwrap();
347 fs::write(marker_file(&tmp), "8").unwrap();
348
349 install_system_skills(tmp.path()).unwrap();
350
351 assert!(skill_file(&tmp, "handoff").is_file());
352 assert!(
353 !skill_file(&tmp, "delegate").exists(),
354 "an intentionally absent older skill must stay absent"
355 );
356 assert_eq!(
357 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
358 BUNDLED_SKILL_VERSION
359 );
360 }
361
362 #[test]
363 fn version_bump_from_v5_adds_best_of_n_without_recreating_deleted_skills() {
364 let tmp = TempDir::new().unwrap();
365 fs::write(marker_file(&tmp), "5").unwrap();
366
367 install_system_skills(tmp.path()).unwrap();
368
369 assert!(skill_file(&tmp, "best-of-n").is_file());
370 assert!(
371 !skill_file(&tmp, "delegate").exists(),
372 "an intentionally absent older skill must stay absent"
373 );
374 assert_eq!(
375 fs::read_to_string(marker_file(&tmp)).unwrap().trim(),
376 BUNDLED_SKILL_VERSION
377 );
378 }
379
380 #[test]
381 fn version_bump_respects_deleted_existing_skill_while_adding_new_skill() {
382 let tmp = TempDir::new().unwrap();
383
384 // Simulate v2 where older bundled skills had been deliberately removed
385 // before later versions introduced more system skills.
386 fs::write(marker_file(&tmp), "2").unwrap();
387
388 install_system_skills(tmp.path()).unwrap();
389
390 assert!(
391 !skill_file(&tmp, "skill-creator").exists(),
392 "version bump should not recreate deleted skill-creator"
393 );
394 assert!(
395 !skill_file(&tmp, "delegate").exists(),
396 "version bump should not recreate deleted delegate"
397 );
398 for skill in BUNDLED_SKILLS
399 .iter()
400 .filter(|skill| skill.introduced_in > 2)
401 {
402 assert!(
403 skill_file(&tmp, skill.name).exists(),
404 "version bump should install newly introduced {}",
405 skill.name
406 );
407 }
408 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
409 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
410 }
411
412 // ── upgrade ───────────────────────────────────────────────────────────────
413
414 #[test]
415 fn upgrade_from_v4_installs_pack_and_retires_unchanged_v4_best_practices() {
416 let tmp = TempDir::new().unwrap();
417 // Simulate a v4 install: marker + legacy skill bodies.
418 fs::create_dir_all(skill_dir(&tmp, "v4-best-practices")).unwrap();
419 fs::write(
420 skill_file(&tmp, "v4-best-practices"),
421 V4_BEST_PRACTICES_BODY,
422 )
423 .unwrap();
424 fs::write(marker_file(&tmp), "4").unwrap();
425
426 install_system_skills(tmp.path()).unwrap();
427
428 assert!(
429 !skill_dir(&tmp, "v4-best-practices").exists(),
430 "unchanged v4-best-practices must be retired"
431 );
432 assert!(skill_file(&tmp, "debug").exists());
433 assert!(skill_file(&tmp, "docx").exists());
434 assert!(skill_file(&tmp, "release").exists());
435 // Feishu is optional — not auto-installed by the default pack.
436 assert!(
437 !skill_dir(&tmp, "feishu").exists(),
438 "feishu must not be universally installed"
439 );
440 let ver = fs::read_to_string(marker_file(&tmp)).unwrap();
441 assert_eq!(ver.trim(), BUNDLED_SKILL_VERSION);
442 }
443
444 #[test]
445 fn upgrade_preserves_modified_v4_best_practices() {
446 let tmp = TempDir::new().unwrap();
447 fs::create_dir_all(skill_dir(&tmp, "v4-best-practices")).unwrap();
448 fs::write(
449 skill_file(&tmp, "v4-best-practices"),
450 "---\nname: v4-best-practices\ndescription: user-owned\n---\n\n# mine\n",
451 )
452 .unwrap();
453 fs::write(marker_file(&tmp), "4").unwrap();
454
455 install_system_skills(tmp.path()).unwrap();
456
457 assert!(skill_dir(&tmp, "v4-best-practices").exists());
458 let body = fs::read_to_string(skill_file(&tmp, "v4-best-practices")).unwrap();
459 assert!(
460 body.contains("user-owned"),
461 "modified body must be preserved"
462 );
463 }
464
465 #[test]
466 fn upgrade_preserves_user_modified_bundled_skill_body() {
467 let tmp = TempDir::new().unwrap();
468 install_system_skills(tmp.path()).unwrap();
469 let path = skill_file(&tmp, "debug");
470 fs::write(
471 &path,
472 "---\nname: debug\ndescription: customized\n---\n\n# custom\n",
473 )
474 .unwrap();
475 // Force version bump attempt
476 fs::write(marker_file(&tmp), "4").unwrap();
477 install_system_skills(tmp.path()).unwrap();
478 let body = fs::read_to_string(path).unwrap();
479 assert!(
480 body.contains("customized"),
481 "user edit must not be overwritten by name alone"
482 );
483 }
484
485 #[test]
486 fn end_user_pack_skills_parse_for_discovery() {
487 let tmp = TempDir::new().unwrap();
488 install_system_skills(tmp.path()).unwrap();
489 let registry = crate::skills::SkillRegistry::discover(tmp.path());
490 assert!(
491 registry.warnings().is_empty(),
492 "bundled skills should parse cleanly: {:?}",
493 registry.warnings()
494 );
495 for name in [
496 "debug", "test", "review", "document", "docx", "release", "plan", "verify",
497 ] {
498 assert!(registry.get(name).is_some(), "{name} must be discoverable");
499 }
500 }
501
502 #[test]
503 fn procedural_skill_homes_remain_bundled_and_lazy() {
504 for name in ["debug", "best-of-n", "simplify", "verify", "test", "review"] {
505 assert!(
506 is_bundled_skill_name(name),
507 "procedural skill home must remain available on demand: {name}"
508 );
509 }
510 }
511
511 lines RUST