1use crate::lesson::{CommandValidation, Difficulty, LessonValidator};
7use crate::stats::UserStats;
8use chrono::{Datelike, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Challenge {
15 pub id: String,
16 pub title: String,
17 pub description: String,
18 pub difficulty: Difficulty,
19 pub challenge_type: ChallengeType,
20 pub points: u32,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum ChallengeType {
26 DailyCommand {
28 command: String,
29 task_description: String,
30 validation: CommandValidation,
31 },
32 WeeklyScenario {
34 scenario: String,
35 steps: Vec<ChallengeStep>,
36 },
37 SpeedChallenge {
39 task: String,
40 time_limit_seconds: u32,
41 commands: Vec<String>,
42 },
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ChallengeStep {
48 pub description: String,
49 pub validation: CommandValidation,
50 pub hint: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ChallengeManager {
56 pub daily_challenge: Option<Challenge>,
58 pub weekly_challenge: Option<Challenge>,
60 pub completed_challenges: HashSet<String>,
62 last_daily_generated: Option<chrono::NaiveDate>,
64 last_weekly_generated: Option<u32>,
66 #[serde(default)]
68 weekly_step_progress: usize,
69}
70
71impl ChallengeManager {
72 pub fn new() -> Self {
74 Self {
75 daily_challenge: None,
76 weekly_challenge: None,
77 completed_challenges: HashSet::new(),
78 last_daily_generated: None,
79 last_weekly_generated: None,
80 weekly_step_progress: 0,
81 }
82 }
83
84 pub fn get_daily_challenge(&mut self) -> Challenge {
86 let today = Utc::now().date_naive();
87
88 if self.last_daily_generated != Some(today) {
90 self.daily_challenge = Some(self.generate_daily_challenge(today));
91 self.last_daily_generated = Some(today);
92 }
93
94 self.daily_challenge.clone().unwrap()
95 }
96
97 pub fn get_weekly_challenge(&mut self) -> Challenge {
99 let now = Utc::now();
100 let current_week = now.iso_week().week();
101
102 if self.last_weekly_generated != Some(current_week) {
104 self.weekly_challenge = Some(self.generate_weekly_challenge(current_week));
105 self.last_weekly_generated = Some(current_week);
106 self.weekly_step_progress = 0;
107 }
108
109 self.weekly_challenge.clone().unwrap()
110 }
111
112 pub fn complete_challenge(&mut self, challenge_id: String) {
119 self.completed_challenges.insert(challenge_id);
120 }
121
122 pub fn check_command(&mut self, command: &str, stats: &mut UserStats) -> Option<Challenge> {
137 let completed = self.match_command(command)?;
138
139 self.complete_challenge(completed.id.clone());
140 stats.complete_challenge(completed.id.clone());
141 stats.record_event("first_challenge");
142
143 Some(completed)
144 }
145
146 fn match_command(&mut self, command: &str) -> Option<Challenge> {
149 let validator = LessonValidator::new();
150
151 if let Some(daily) = &self.daily_challenge {
153 if !self.is_challenge_completed(&daily.id)
154 && Self::command_satisfies(&validator, daily, command)
155 {
156 return Some(daily.clone());
157 }
158 }
159
160 if let Some(weekly) = self.weekly_challenge.clone() {
162 if !self.is_challenge_completed(&weekly.id) {
163 if let ChallengeType::WeeklyScenario { steps, .. } = &weekly.challenge_type {
164 if let Some(step) = steps.get(self.weekly_step_progress) {
165 if validator
168 .validate_command(command, "", &step.validation)
169 .is_success()
170 {
171 self.weekly_step_progress += 1;
172 if self.weekly_step_progress >= steps.len() {
173 return Some(weekly);
174 }
175 }
176 }
177 }
178 }
179 }
180
181 None
182 }
183
184 fn command_satisfies(validator: &LessonValidator, challenge: &Challenge, command: &str) -> bool {
186 match &challenge.challenge_type {
187 ChallengeType::DailyCommand {
188 command: expected,
189 validation,
190 ..
191 } => validator.validate_command(command, expected, validation).is_success(),
192 ChallengeType::SpeedChallenge { commands, .. } => {
193 let normalize = |s: &str| s.split_whitespace().collect::<Vec<_>>().join(" ");
194 let input = normalize(command);
195 commands.iter().any(|c| normalize(c) == input)
196 }
197 ChallengeType::WeeklyScenario { .. } => false,
199 }
200 }
201
202 pub fn is_challenge_completed(&self, challenge_id: &str) -> bool {
204 self.completed_challenges.contains(challenge_id)
205 }
206
207 fn generate_daily_challenge(&self, date: chrono::NaiveDate) -> Challenge {
209 let all_daily = all_daily_challenges();
210 let index = (date.ordinal0() as usize) % all_daily.len();
212 all_daily[index].clone()
213 }
214
215 fn generate_weekly_challenge(&self, week: u32) -> Challenge {
217 let all_weekly = all_weekly_challenges();
218 let index = (week as usize - 1) % all_weekly.len();
220 all_weekly[index].clone()
221 }
222
223 pub fn weekly_completions(&self) -> usize {
225 self.completed_challenges.len()
228 }
229
230 pub fn is_daily_completed(&self) -> bool {
232 if let Some(ref daily) = self.daily_challenge {
233 self.is_challenge_completed(&daily.id)
234 } else {
235 false
236 }
237 }
238
239 pub fn is_weekly_completed(&self) -> bool {
241 if let Some(ref weekly) = self.weekly_challenge {
242 self.is_challenge_completed(&weekly.id)
243 } else {
244 false
245 }
246 }
247}
248
249impl Default for ChallengeManager {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub fn all_daily_challenges() -> Vec<Challenge> {
257 vec![
258 Challenge {
259 id: "daily_grep_recursive".to_string(),
260 title: "Master Recursive Search".to_string(),
261 description: "Use grep with the -r flag to search for 'error' in all files within a directory.".to_string(),
262 difficulty: Difficulty::Intermediate,
263 challenge_type: ChallengeType::DailyCommand {
264 command: "grep -r 'error' /var/log".to_string(),
265 task_description: "Search recursively for the word 'error' in log files".to_string(),
266 validation: CommandValidation::CommandAndFlags,
267 },
268 points: 25,
269 },
270 Challenge {
271 id: "daily_chmod_executable".to_string(),
272 title: "Make It Executable".to_string(),
273 description: "Use chmod to make a script file executable by adding execute permissions.".to_string(),
274 difficulty: Difficulty::Beginner,
275 challenge_type: ChallengeType::DailyCommand {
276 command: "chmod +x script.sh".to_string(),
277 task_description: "Make a file executable using chmod".to_string(),
278 validation: CommandValidation::CommandAndFlags,
279 },
280 points: 20,
281 },
282 Challenge {
283 id: "daily_find_by_name".to_string(),
284 title: "Find by Name".to_string(),
285 description: "Use the find command to locate all .txt files in your home directory.".to_string(),
286 difficulty: Difficulty::Intermediate,
287 challenge_type: ChallengeType::DailyCommand {
288 command: "find ~ -name '*.txt'".to_string(),
289 task_description: "Find all text files in home directory".to_string(),
290 validation: CommandValidation::CommandAndFlags,
291 },
292 points: 25,
293 },
294 Challenge {
295 id: "daily_pipe_mastery".to_string(),
296 title: "Pipe Master".to_string(),
297 description: "Combine ls and grep using a pipe to find files containing 'test' in their names.".to_string(),
298 difficulty: Difficulty::Beginner,
299 challenge_type: ChallengeType::DailyCommand {
300 command: "ls | grep test".to_string(),
301 task_description: "Use pipes to filter ls output with grep".to_string(),
302 validation: CommandValidation::CommandAndFlags,
303 },
304 points: 20,
305 },
306 Challenge {
307 id: "daily_tar_archive".to_string(),
308 title: "Archive Creation".to_string(),
309 description: "Create a compressed tar archive of a directory using tar -czf.".to_string(),
310 difficulty: Difficulty::Intermediate,
311 challenge_type: ChallengeType::DailyCommand {
312 command: "tar -czf archive.tar.gz directory/".to_string(),
313 task_description: "Create a compressed tar archive".to_string(),
314 validation: CommandValidation::CommandAndFlags,
315 },
316 points: 30,
317 },
318 Challenge {
319 id: "daily_curl_download".to_string(),
320 title: "Web Fetcher".to_string(),
321 description: "Use curl to download a file from a URL and save it with -o option.".to_string(),
322 difficulty: Difficulty::Intermediate,
323 challenge_type: ChallengeType::DailyCommand {
324 command: "curl -o output.html https://example.com".to_string(),
325 task_description: "Download a file using curl".to_string(),
326 validation: CommandValidation::CommandAndFlags,
327 },
328 points: 25,
329 },
330 Challenge {
331 id: "daily_process_check".to_string(),
332 title: "Process Inspector".to_string(),
333 description: "Use ps aux combined with grep to find a specific running process.".to_string(),
334 difficulty: Difficulty::Intermediate,
335 challenge_type: ChallengeType::DailyCommand {
336 command: "ps aux | grep firefox".to_string(),
337 task_description: "Find a running process by name".to_string(),
338 validation: CommandValidation::CommandAndFlags,
339 },
340 points: 25,
341 },
342 Challenge {
343 id: "daily_disk_usage".to_string(),
344 title: "Disk Usage Detective".to_string(),
345 description: "Use du -sh to find the size of a directory in human-readable format.".to_string(),
346 difficulty: Difficulty::Beginner,
347 challenge_type: ChallengeType::DailyCommand {
348 command: "du -sh /var/log".to_string(),
349 task_description: "Check directory size with du".to_string(),
350 validation: CommandValidation::CommandAndFlags,
351 },
352 points: 20,
353 },
354 Challenge {
355 id: "daily_sed_replace".to_string(),
356 title: "Text Transformer".to_string(),
357 description: "Use sed to replace all occurrences of a word in a file.".to_string(),
358 difficulty: Difficulty::Advanced,
359 challenge_type: ChallengeType::DailyCommand {
360 command: "sed -i 's/old/new/g' file.txt".to_string(),
361 task_description: "Replace text in a file using sed".to_string(),
362 validation: CommandValidation::CommandAndFlags,
363 },
364 points: 35,
365 },
366 Challenge {
367 id: "daily_symlink_create".to_string(),
368 title: "Link Creator".to_string(),
369 description: "Create a symbolic link using ln -s pointing to a target file.".to_string(),
370 difficulty: Difficulty::Beginner,
371 challenge_type: ChallengeType::DailyCommand {
372 command: "ln -s /path/to/target linkname".to_string(),
373 task_description: "Create a symbolic link".to_string(),
374 validation: CommandValidation::CommandAndFlags,
375 },
376 points: 20,
377 },
378 Challenge {
379 id: "daily_history_search".to_string(),
380 title: "History Hunter".to_string(),
381 description: "Use history with grep to find commands you've run in the past.".to_string(),
382 difficulty: Difficulty::Beginner,
383 challenge_type: ChallengeType::DailyCommand {
384 command: "history | grep git".to_string(),
385 task_description: "Search command history".to_string(),
386 validation: CommandValidation::CommandAndFlags,
387 },
388 points: 15,
389 },
390 Challenge {
391 id: "daily_wc_count".to_string(),
392 title: "Word Counter".to_string(),
393 description: "Count the number of lines in a file using wc -l.".to_string(),
394 difficulty: Difficulty::Beginner,
395 challenge_type: ChallengeType::DailyCommand {
396 command: "wc -l file.txt".to_string(),
397 task_description: "Count lines in a file".to_string(),
398 validation: CommandValidation::CommandAndFlags,
399 },
400 points: 15,
401 },
402 Challenge {
403 id: "daily_env_check".to_string(),
404 title: "Environment Explorer".to_string(),
405 description: "Use env or printenv to display all environment variables.".to_string(),
406 difficulty: Difficulty::Beginner,
407 challenge_type: ChallengeType::DailyCommand {
408 command: "env".to_string(),
409 task_description: "Display environment variables".to_string(),
410 validation: CommandValidation::CommandOnly,
411 },
412 points: 15,
413 },
414 Challenge {
415 id: "daily_tail_follow".to_string(),
416 title: "Log Follower".to_string(),
417 description: "Use tail -f to continuously monitor a log file in real-time.".to_string(),
418 difficulty: Difficulty::Intermediate,
419 challenge_type: ChallengeType::DailyCommand {
420 command: "tail -f /var/log/syslog".to_string(),
421 task_description: "Follow a log file in real-time".to_string(),
422 validation: CommandValidation::CommandAndFlags,
423 },
424 points: 25,
425 },
426 Challenge {
427 id: "daily_awk_column".to_string(),
428 title: "Column Extractor".to_string(),
429 description: "Use awk to extract a specific column from formatted text output.".to_string(),
430 difficulty: Difficulty::Advanced,
431 challenge_type: ChallengeType::DailyCommand {
432 command: "ps aux | awk '{print $1, $11}'".to_string(),
433 task_description: "Extract specific columns using awk".to_string(),
434 validation: CommandValidation::CommandAndFlags,
435 },
436 points: 35,
437 },
438 ]
439}
440
441pub fn all_weekly_challenges() -> Vec<Challenge> {
443 vec![
444 Challenge {
445 id: "weekly_debug_server".to_string(),
446 title: "Debug the Broken Server".to_string(),
447 description: "A simulated web server isn't starting. Use diagnostic commands to find and fix the issues.".to_string(),
448 difficulty: Difficulty::Advanced,
449 challenge_type: ChallengeType::WeeklyScenario {
450 scenario: "The web server on port 8080 isn't responding. Debug the issue step by step.".to_string(),
451 steps: vec![
452 ChallengeStep {
453 description: "Check if the server process is running".to_string(),
454 validation: CommandValidation::Regex("ps.*8080|netstat.*8080|lsof.*8080".to_string()),
455 hint: Some("Try 'ps aux | grep server' or 'netstat -tulpn'".to_string()),
456 },
457 ChallengeStep {
458 description: "Check the server log files for errors".to_string(),
459 validation: CommandValidation::Regex("tail.*log|cat.*log|less.*log".to_string()),
460 hint: Some("Look in /var/log/ or check application-specific logs".to_string()),
461 },
462 ChallengeStep {
463 description: "Verify the port is not in use by another process".to_string(),
464 validation: CommandValidation::Regex("netstat.*8080|lsof.*8080|ss.*8080".to_string()),
465 hint: Some("Use 'lsof -i :8080' or 'netstat -tulpn | grep 8080'".to_string()),
466 },
467 ],
468 },
469 points: 100,
470 },
471 Challenge {
472 id: "weekly_organize_files".to_string(),
473 title: "Organize the Chaos".to_string(),
474 description: "A directory is filled with mixed file types. Organize them into proper subdirectories.".to_string(),
475 difficulty: Difficulty::Intermediate,
476 challenge_type: ChallengeType::WeeklyScenario {
477 scenario: "The downloads folder has 100+ files of different types all mixed together. Organize them!".to_string(),
478 steps: vec![
479 ChallengeStep {
480 description: "Create directories for different file types (images, documents, code)".to_string(),
481 validation: CommandValidation::Regex("mkdir.*images|mkdir.*documents|mkdir.*code".to_string()),
482 hint: Some("Use 'mkdir -p' to create multiple directories".to_string()),
483 },
484 ChallengeStep {
485 description: "Find and move all image files to the images directory".to_string(),
486 validation: CommandValidation::Regex("mv.*\\.(jpg|png|gif)|find.*\\.(jpg|png).*-exec mv".to_string()),
487 hint: Some("Use 'find' with '-name' or a for loop with 'mv'".to_string()),
488 },
489 ChallengeStep {
490 description: "Create an archive of the organized directories".to_string(),
491 validation: CommandValidation::Regex("tar.*czf|zip.*-r".to_string()),
492 hint: Some("Use 'tar -czf organized.tar.gz' or 'zip -r'".to_string()),
493 },
494 ],
495 },
496 points: 80,
497 },
498 Challenge {
499 id: "weekly_security_audit".to_string(),
500 title: "Security Audit".to_string(),
501 description: "Perform a basic security audit by checking file permissions and user access.".to_string(),
502 difficulty: Difficulty::Advanced,
503 challenge_type: ChallengeType::WeeklyScenario {
504 scenario: "Audit file permissions and find potential security issues in a system directory.".to_string(),
505 steps: vec![
506 ChallengeStep {
507 description: "Find all world-writable files".to_string(),
508 validation: CommandValidation::Regex("find.*-perm.*777|find.*-perm.*o=w".to_string()),
509 hint: Some("Use 'find /path -perm -002'".to_string()),
510 },
511 ChallengeStep {
512 description: "List all users who can access the system".to_string(),
513 validation: CommandValidation::Regex("cat /etc/passwd|getent passwd|cut.*passwd".to_string()),
514 hint: Some("Check /etc/passwd or use 'getent passwd'".to_string()),
515 },
516 ChallengeStep {
517 description: "Check for files with setuid bit enabled".to_string(),
518 validation: CommandValidation::Regex("find.*-perm.*4000|find.*-perm.*u=s".to_string()),
519 hint: Some("Use 'find / -perm -4000'".to_string()),
520 },
521 ],
522 },
523 points: 120,
524 },
525 Challenge {
526 id: "weekly_backup_strategy".to_string(),
527 title: "Backup Strategy".to_string(),
528 description: "Create a comprehensive backup of important files with proper organization.".to_string(),
529 difficulty: Difficulty::Intermediate,
530 challenge_type: ChallengeType::WeeklyScenario {
531 scenario: "Set up an automated backup system for critical directories.".to_string(),
532 steps: vec![
533 ChallengeStep {
534 description: "Create a timestamped backup directory".to_string(),
535 validation: CommandValidation::Regex("mkdir.*backup.*$(date|date.*\\+)".to_string()),
536 hint: Some("Use 'mkdir backup-$(date +%Y%m%d)'".to_string()),
537 },
538 ChallengeStep {
539 description: "Copy important files preserving permissions and timestamps".to_string(),
540 validation: CommandValidation::Regex("cp -a|cp.*-p|rsync.*-a".to_string()),
541 hint: Some("Use 'cp -a' or 'rsync -av'".to_string()),
542 },
543 ChallengeStep {
544 description: "Create a compressed archive of the backup".to_string(),
545 validation: CommandValidation::Regex("tar.*czf|gzip|zip".to_string()),
546 hint: Some("Use 'tar -czf backup.tar.gz backup/'".to_string()),
547 },
548 ],
549 },
550 points: 90,
551 },
552 Challenge {
553 id: "weekly_log_analysis".to_string(),
554 title: "Log Analysis Detective".to_string(),
555 description: "Analyze system logs to find errors, warnings, and unusual patterns.".to_string(),
556 difficulty: Difficulty::Advanced,
557 challenge_type: ChallengeType::WeeklyScenario {
558 scenario: "Investigate system logs to identify and report issues.".to_string(),
559 steps: vec![
560 ChallengeStep {
561 description: "Find all error messages in system logs".to_string(),
562 validation: CommandValidation::Regex("grep.*error|grep.*ERROR|egrep.*'error|ERROR'".to_string()),
563 hint: Some("Use 'grep -i error /var/log/syslog'".to_string()),
564 },
565 ChallengeStep {
566 description: "Count occurrences of different error types".to_string(),
567 validation: CommandValidation::Regex("sort|uniq -c|wc".to_string()),
568 hint: Some("Combine 'sort | uniq -c | sort -n'".to_string()),
569 },
570 ChallengeStep {
571 description: "Extract and save the top 10 most frequent errors to a file".to_string(),
572 validation: CommandValidation::Regex("head.*>|>.*head|tee".to_string()),
573 hint: Some("Use pipes and redirect with '> errors.txt'".to_string()),
574 },
575 ],
576 },
577 points: 110,
578 },
579 ]
580}
581
582pub fn all_speed_challenges() -> Vec<Challenge> {
584 vec![
585 Challenge {
586 id: "speed_cleanup_tmp".to_string(),
587 title: "Rapid Cleanup".to_string(),
588 description: "Find and delete all .tmp files in a directory tree within 60 seconds.".to_string(),
589 difficulty: Difficulty::Intermediate,
590 challenge_type: ChallengeType::SpeedChallenge {
591 task: "Remove all temporary files (.tmp extension) from the current directory and subdirectories".to_string(),
592 time_limit_seconds: 60,
593 commands: vec![
594 "find . -name '*.tmp' -delete".to_string(),
595 "find . -name '*.tmp' -exec rm {} \\;".to_string(),
596 ],
597 },
598 points: 50,
599 },
600 Challenge {
601 id: "speed_count_files".to_string(),
602 title: "Quick Count".to_string(),
603 description: "Count all files in a directory within 30 seconds.".to_string(),
604 difficulty: Difficulty::Beginner,
605 challenge_type: ChallengeType::SpeedChallenge {
606 task: "Count the total number of files (not directories) in the current directory tree".to_string(),
607 time_limit_seconds: 30,
608 commands: vec![
609 "find . -type f | wc -l".to_string(),
610 "ls -lR | grep '^-' | wc -l".to_string(),
611 ],
612 },
613 points: 40,
614 },
615 Challenge {
616 id: "speed_largest_files".to_string(),
617 title: "Size Hunter".to_string(),
618 description: "Find the 5 largest files in a directory within 45 seconds.".to_string(),
619 difficulty: Difficulty::Intermediate,
620 challenge_type: ChallengeType::SpeedChallenge {
621 task: "List the 5 largest files in the directory tree with their sizes".to_string(),
622 time_limit_seconds: 45,
623 commands: vec![
624 "find . -type f -exec ls -lh {} \\; | sort -k5 -hr | head -5".to_string(),
625 "du -ah . | sort -rh | head -5".to_string(),
626 ],
627 },
628 points: 55,
629 },
630 ]
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn test_challenge_manager_creation() {
639 let manager = ChallengeManager::new();
640 assert!(manager.daily_challenge.is_none());
641 assert!(manager.weekly_challenge.is_none());
642 assert_eq!(manager.completed_challenges.len(), 0);
643 }
644
645 #[test]
646 fn test_daily_challenge_generation() {
647 let mut manager = ChallengeManager::new();
648 let challenge = manager.get_daily_challenge();
649 assert!(!challenge.id.is_empty());
650 assert!(!challenge.title.is_empty());
651 assert!(challenge.points > 0);
652 }
653
654 #[test]
655 fn test_weekly_challenge_generation() {
656 let mut manager = ChallengeManager::new();
657 let challenge = manager.get_weekly_challenge();
658 assert!(!challenge.id.is_empty());
659 assert!(!challenge.title.is_empty());
660 assert!(challenge.points > 0);
661 }
662
663 #[test]
664 fn test_challenge_completion() {
665 let mut manager = ChallengeManager::new();
666 assert!(!manager.is_challenge_completed("test_challenge"));
667
668 manager.complete_challenge("test_challenge".to_string());
669 assert!(manager.is_challenge_completed("test_challenge"));
670 }
671
672 #[test]
673 fn test_daily_challenges_exist() {
674 let challenges = all_daily_challenges();
675 assert!(!challenges.is_empty());
676 assert!(challenges.len() >= 10);
677 }
678
679 #[test]
680 fn test_weekly_challenges_exist() {
681 let challenges = all_weekly_challenges();
682 assert!(!challenges.is_empty());
683 assert!(challenges.len() >= 5);
684 }
685
686 #[test]
687 fn test_deterministic_daily_selection() {
688 let mut manager1 = ChallengeManager::new();
689 let mut manager2 = ChallengeManager::new();
690
691 let challenge1 = manager1.get_daily_challenge();
692 let challenge2 = manager2.get_daily_challenge();
693
694 assert_eq!(challenge1.id, challenge2.id);
696 }
697
698 #[test]
699 fn test_check_command_completes_daily_challenge() {
700 let mut manager = ChallengeManager::new();
701 let mut stats = UserStats::new();
702 let daily = manager.get_daily_challenge();
703
704 assert!(manager.check_command("echo hello", &mut stats).is_none());
706 assert!(!manager.is_daily_completed());
707
708 let expected = match &daily.challenge_type {
710 ChallengeType::DailyCommand { command, .. } => command.clone(),
711 other => panic!("daily challenge should be DailyCommand, got {:?}", other),
712 };
713 let completed = manager.check_command(&expected, &mut stats).unwrap();
714 assert_eq!(completed.id, daily.id);
715 assert!(manager.is_daily_completed());
716
717 assert!(stats.completed_challenges.contains(&daily.id));
719 assert!(stats.completed_challenges.contains("first_challenge"));
720 assert!(stats.achievements.is_unlocked("challenge_accepted"));
721
722 assert!(manager.check_command(&expected, &mut stats).is_none());
724 }
725
726 #[test]
727 fn test_check_command_weekly_scenario_steps() {
728 let mut manager = ChallengeManager::new();
729 let mut stats = UserStats::new();
730
731 let weekly = all_weekly_challenges()[0].clone();
733 assert_eq!(weekly.id, "weekly_debug_server");
734 manager.weekly_challenge = Some(weekly.clone());
735
736 assert!(manager.check_command("ps aux | grep 8080", &mut stats).is_none());
738 assert!(manager.check_command("tail -f /var/log/server.log", &mut stats).is_none());
740 assert!(manager.check_command("echo not a match", &mut stats).is_none());
742 let completed = manager.check_command("lsof -i :8080", &mut stats).unwrap();
744 assert_eq!(completed.id, weekly.id);
745 assert!(manager.is_weekly_completed());
746 assert!(stats.completed_challenges.contains(&weekly.id));
747 }
748}