diff --git a/app/Commands/DailyReviewCommand.php b/app/Commands/DailyReviewCommand.php new file mode 100644 index 0000000..c2ad9bb --- /dev/null +++ b/app/Commands/DailyReviewCommand.php @@ -0,0 +1,502 @@ +hasTodayReview()) { + $this->error('You have already completed a daily review today.'); + + return self::FAILURE; + } + + $isQuick = (bool) $this->option('quick'); + + // Display header + $this->displayHeader(); + + // Retrieve and display milestones + $milestones = $this->getTodayMilestones(); + $this->displayMilestones($milestones); + + // Prompt for reflections + $reflections = $this->promptReflections($isQuick); + + // Save entry + $entry = $this->saveEntry($reflections, $milestones, $isQuick); + + // Display success + $this->newLine(); + $this->info("✓ Daily review saved successfully as entry #{$entry->id}"); + + return self::SUCCESS; + } + + private function hasTodayReview(): bool + { + return Entry::query() + ->where('category', 'reflection') + ->where('tags', 'like', '%daily-review%') + ->whereDate('created_at', today()) + ->exists(); + } + + private function displayHeader(): void + { + $this->newLine(); + $this->line('Daily Review - '.now()->format('l, F j, Y').''); + $this->newLine(); + } + + /** + * @return array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } + */ + private function getTodayMilestones(): array + { + return [ + 'knowledge' => $this->getKnowledgeMilestones(), + 'prs' => $this->getMergedPRsToday(), + 'issues' => $this->getClosedIssuesToday(), + ]; + } + + /** + * @return Collection + */ + private function getKnowledgeMilestones(): Collection + { + /** @var Collection */ + return Entry::query() + ->where(function ($query) { + $query->where('content', 'like', '%✅%') + ->orWhere('content', 'like', '%## Milestones%') + ->orWhere('tags', 'like', '%milestone%'); + }) + ->where('status', 'validated') + ->whereDate('created_at', today()) + ->orderBy('created_at', 'desc') + ->get(); + } + + /** + * @return array> + */ + private function getMergedPRsToday(): array + { + $todayStart = now()->startOfDay()->toIso8601String(); + $repoName = $this->getRepoName(); + + if ($repoName === null) { + return []; + } + + $args = [ + 'gh', 'pr', 'list', + '--repo', $repoName, + '--state', 'merged', + '--json', 'number,title,mergedAt,url', + '--limit', '100', + ]; + + $result = Process::run($args); + + if (! $result->successful()) { + return []; + } + + $prs = json_decode(trim($result->output()), true); + + if (! is_array($prs)) { + return []; + } + + return array_values(array_filter($prs, function ($pr) use ($todayStart) { + $mergedAt = $pr['mergedAt'] ?? null; + + return $mergedAt && $mergedAt >= $todayStart; + })); + } + + /** + * @return array> + */ + private function getClosedIssuesToday(): array + { + $todayStart = now()->startOfDay()->toIso8601String(); + $repoName = $this->getRepoName(); + + if ($repoName === null) { + return []; + } + + $args = [ + 'gh', 'issue', 'list', + '--repo', $repoName, + '--state', 'closed', + '--json', 'number,title,closedAt,url', + '--limit', '100', + ]; + + $result = Process::run($args); + + if (! $result->successful()) { + return []; + } + + $issues = json_decode(trim($result->output()), true); + + if (! is_array($issues)) { + return []; + } + + return array_values(array_filter($issues, function ($issue) use ($todayStart) { + $closedAt = $issue['closedAt'] ?? null; + + return $closedAt && $closedAt >= $todayStart; + })); + } + + private function getRepoName(): ?string + { + $result = Process::run(['git', 'remote', 'get-url', 'origin']); + + if (! $result->successful()) { + return null; + } + + $remote = trim($result->output()); + + if (preg_match('#github\.com[:/](.+/.+?)(?:\.git)?$#', $remote, $matches) === 1) { + return $matches[1]; + } + + return null; + } + + /** + * @param array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } $milestones + */ + private function displayMilestones(array $milestones): void + { + $hasKnowledge = $milestones['knowledge']->isNotEmpty(); + $hasPRs = $milestones['prs'] !== []; + $hasIssues = $milestones['issues'] !== []; + + if (! $hasKnowledge && ! $hasPRs && ! $hasIssues) { + $this->line('No milestones found'); + $this->newLine(); + + return; + } + + $this->line("Today's Accomplishments"); + $this->newLine(); + + if ($hasKnowledge) { + $this->displayKnowledgeMilestones($milestones['knowledge']); + } + + if ($hasPRs) { + $this->displayMergedPRs($milestones['prs']); + } + + if ($hasIssues) { + $this->displayClosedIssues($milestones['issues']); + } + + $this->newLine(); + } + + /** + * @param Collection $milestones + */ + private function displayKnowledgeMilestones(Collection $milestones): void + { + $this->line('Knowledge Milestones:'); + + foreach ($milestones as $milestone) { + $this->line(" ✓ {$milestone->title}"); + + $details = $this->extractMilestoneDetails($milestone); + foreach ($details as $detail) { + $this->line(" • {$detail}"); + } + } + + $this->newLine(); + } + + /** + * @return array + */ + private function extractMilestoneDetails(Entry $milestone): array + { + $details = []; + $content = $milestone->content; + + if (preg_match('/## Milestones(.+?)(?=##|$)/s', $content, $matches) === 1) { + $milestonesSection = $matches[1]; + + if (preg_match_all('/[-•]\s*✅\s*(.+?)(?=\n|$)/u', $milestonesSection, $itemMatches) > 0) { + foreach ($itemMatches[1] as $item) { + $details[] = trim($item); + } + } + + if (empty($details) && preg_match_all('/✅\s*(.+?)(?=\n|$)/u', $milestonesSection, $checkMatches) > 0) { + foreach ($checkMatches[1] as $item) { + $cleaned = trim($item); + if (! in_array($cleaned, $details, true)) { + $details[] = $cleaned; + } + } + } + } + + return $details; + } + + /** + * @param array> $prs + */ + private function displayMergedPRs(array $prs): void + { + $this->line('Merged Pull Requests:'); + + foreach ($prs as $pr) { + $this->line(" ✓ #{$pr['number']} {$pr['title']}"); + } + + $this->newLine(); + } + + /** + * @param array> $issues + */ + private function displayClosedIssues(array $issues): void + { + $this->line('Closed Issues:'); + + foreach ($issues as $issue) { + $this->line(" ✓ #{$issue['number']} {$issue['title']}"); + } + + $this->newLine(); + } + + /** + * @return array + */ + private function promptReflections(bool $isQuick): array + { + $reflections = []; + + if ($isQuick) { + $reflections['what_went_well'] = (string) $this->ask('What went well today?'); + $reflections['what_learned'] = (string) $this->ask('What did you learn?'); + $reflections['key_takeaways'] = (string) $this->ask('What are your key takeaways?'); + } else { + $reflections['what_went_well'] = (string) $this->ask('What went well today?'); + $reflections['biggest_challenges'] = (string) $this->ask('What were the biggest challenges?'); + $reflections['what_learned'] = (string) $this->ask('What did you learn?'); + $reflections['do_differently'] = (string) $this->ask('What would you do differently?'); + $reflections['key_takeaways'] = (string) $this->ask('What are your key takeaways?'); + } + + return $reflections; + } + + /** + * @param array $reflections + * @param array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } $milestones + */ + private function saveEntry(array $reflections, array $milestones, bool $isQuick): Entry + { + $content = $this->formatContent($reflections, $milestones, $isQuick); + $title = $this->generateTitle($milestones); + $tags = $this->generateTags($isQuick); + + /** @var Entry */ + return Entry::create([ + 'title' => $title, + 'content' => $content, + 'category' => 'reflection', + 'tags' => $tags, + 'status' => 'validated', + 'confidence' => 95, + 'validation_date' => now(), + 'repo' => $this->getCurrentRepo(), + 'branch' => $this->getCurrentBranch(), + 'commit' => $this->getCurrentCommit(), + ]); + } + + /** + * @param array $reflections + * @param array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } $milestones + */ + private function formatContent(array $reflections, array $milestones, bool $isQuick): string + { + $content = []; + + // Add milestones section + $milestonesList = $this->formatMilestonesList($milestones); + if (! empty($milestonesList)) { + $content[] = "## Milestones\n".implode("\n", $milestonesList); + } + + // Add reflection sections + if (isset($reflections['what_went_well'])) { + $content[] = "## What Went Well\n".$reflections['what_went_well']; + } + + if (isset($reflections['biggest_challenges'])) { + $content[] = "## Biggest Challenges\n".$reflections['biggest_challenges']; + } + + if (isset($reflections['what_learned'])) { + $content[] = "## What I Learned\n".$reflections['what_learned']; + } + + if (isset($reflections['do_differently'])) { + $content[] = "## What I Would Do Differently\n".$reflections['do_differently']; + } + + if (isset($reflections['key_takeaways'])) { + $content[] = "## Key Takeaways\n".$reflections['key_takeaways']; + } + + return implode("\n\n", $content); + } + + /** + * @param array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } $milestones + * @return array + */ + private function formatMilestonesList(array $milestones): array + { + $list = []; + + foreach ($milestones['knowledge'] as $milestone) { + $list[] = "- ✅ {$milestone->title}"; + } + + foreach ($milestones['prs'] as $pr) { + $list[] = "- ✅ PR #{$pr['number']}: {$pr['title']}"; + } + + foreach ($milestones['issues'] as $issue) { + $list[] = "- ✅ Issue #{$issue['number']}: {$issue['title']}"; + } + + return $list; + } + + /** + * @param array{ + * knowledge: Collection, + * prs: array>, + * issues: array> + * } $milestones + */ + private function generateTitle(array $milestones): string + { + $date = now()->format('Y-m-d'); + $count = $milestones['knowledge']->count() + count($milestones['prs']) + count($milestones['issues']); + + if ($count > 0) { + return "Daily Review - {$date} ({$count} milestones)"; + } + + return "Daily Review - {$date}"; + } + + /** + * @return array + */ + private function generateTags(bool $isQuick): array + { + $tags = ['daily-review', now()->format('Y-m-d')]; + + if ($isQuick) { + $tags[] = 'quick'; + } + + return $tags; + } + + private function getCurrentRepo(): ?string + { + $result = Process::run(['git', 'remote', 'get-url', 'origin']); + + if (! $result->successful()) { + return null; + } + + return trim($result->output()); + } + + private function getCurrentBranch(): ?string + { + try { + $result = Process::run(['git', 'branch', '--show-current']); + + if (! $result->successful()) { + return null; + } + + return trim($result->output()); + } catch (\Throwable $e) { + return null; + } + } + + private function getCurrentCommit(): ?string + { + try { + $result = Process::run(['git', 'rev-parse', 'HEAD']); + + if (! $result->successful()) { + return null; + } + + return trim($result->output()); + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/app/Commands/Session/StartCommand.php b/app/Commands/Session/StartCommand.php index a0a86e6..b861713 100644 --- a/app/Commands/Session/StartCommand.php +++ b/app/Commands/Session/StartCommand.php @@ -12,7 +12,8 @@ class StartCommand extends Command { protected $signature = 'session:start - {--json : Output as JSON instead of markdown}'; + {--json : Output as JSON instead of markdown} + {--patterns : Show only power user patterns}'; protected $description = 'Start a new session and output context for Claude Code hooks'; @@ -32,7 +33,16 @@ public function handle(): int $this->storeSessionId($session->id); // Output context for Claude - if ($this->option('json')) { + /** @var bool $patterns */ + $patterns = $this->option('patterns'); + /** @var bool $json */ + $json = $this->option('json'); + + if ($patterns && $json) { + $this->outputPatternsJson(); + } elseif ($patterns) { + $this->outputPatternsMarkdown(); + } elseif ($json) { $this->outputJson($project, $branch, $session->id); } else { $this->outputMarkdown($project, $branch, $session->id); @@ -75,7 +85,7 @@ private function storeSessionId(string $sessionId): void } // Also store in temp file as fallback - $tempFile = sys_get_temp_dir() . '/know-session-id'; + $tempFile = sys_get_temp_dir().'/know-session-id'; file_put_contents($tempFile, $sessionId); } @@ -100,7 +110,7 @@ private function outputMarkdown(string $project, ?string $branch, string $sessio // Last commit $lastCommit = shell_exec('git log -1 --oneline 2>/dev/null'); if (is_string($lastCommit) && trim($lastCommit) !== '') { - $output[] = "- **Last commit:** " . trim($lastCommit); + $output[] = '- **Last commit:** '.trim($lastCommit); } // Branch commits (if on feature branch) @@ -119,6 +129,10 @@ private function outputMarkdown(string $project, ?string $branch, string $sessio } } + // Power user patterns + $output[] = ''; + $output = array_merge($output, $this->getPowerUserPatterns()); + // Recent relevant knowledge (semantic search, excludes session noise) $knowledge = $this->getRelevantKnowledge($project); if (count($knowledge) > 0) { @@ -127,7 +141,7 @@ private function outputMarkdown(string $project, ?string $branch, string $sessio foreach ($knowledge as $entry) { $output[] = "- **{$entry['title']}** (confidence: {$entry['confidence']}%)"; if ($entry['content'] !== '') { - $output[] = " " . $entry['content'] . '...'; + $output[] = ' '.$entry['content'].'...'; } } } @@ -152,6 +166,7 @@ private function outputJson(string $project, ?string $branch, string $sessionId) 'started_at' => now()->toIso8601String(), 'git' => $this->getGitContext(), 'knowledge' => $this->getRelevantKnowledge($project), + 'power_user_patterns' => $this->getPowerUserPatternsArray(), ]; $this->line(json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); @@ -231,4 +246,131 @@ private function getLastSessionSummary(string $project): ?string return $lastSession?->summary; } + + /** + * @return array + */ + private function getPowerUserPatterns(): array + { + return [ + '## 🧠 Know Before You Act - Power User Patterns', + '', + '### Daily Rituals', + '**Morning (5 min)**', + '- ./know priorities → See top 3 blockers/intents', + '- ./know context → Load project-specific knowledge', + '- ./know blockers --project=X → Check what\'s blocking progress', + '', + '**Focus Block (2-4 hours)**', + '- ./know focus-time → Declare focus block', + ' → Tracks context switches automatically', + ' → Measures effectiveness (0-10 score)', + ' → Prompts energy before/after', + '', + '**Evening (10 min)**', + '- ./know daily-review → Structured reflection', + ' → Auto-pulls merged PRs + closed issues', + ' → 5 reflection questions', + ' → Saves as validated entry (confidence: 95)', + '', + '### Context Loading', + '- Before coding: ./know context or ./know session:start', + '- Check blockers: ./know blockers --project=prefrontal-cortex', + '- Recent wins: ./know milestones --today', + '- Recent work: ./know intents --recent', + '', + '### Search Patterns', + '- Already solved? ./know search "authentication flow"', + '- High confidence only: ./know search --confidence=80', + '- By category: ./know search --category=debugging', + '- Semantic search: ./know search --semantic "error handling"', + '', + '### Anti-Patterns (Learn from Mistakes)', + '❌ Ship 5 PRs → ask "what am I missing" → no reflection', + '✅ Ship 5 PRs → ./know daily-review → celebrate → extract learning → plan tomorrow', + '', + '❌ Context switch 20+ times → scattered focus → low effectiveness', + '✅ ./know focus-time prefrontal-cortex 3 → declare block → track switches → adjust behavior', + '', + '❌ Start coding immediately → miss existing solutions → duplicate work', + '✅ ./know search first → leverage past work → avoid duplication → ship faster', + ]; + } + + /** + * @return array + */ + private function getPowerUserPatternsArray(): array + { + return [ + 'daily_rituals' => [ + 'morning' => [ + 'duration' => '5 min', + 'commands' => [ + './know priorities → See top 3 blockers/intents', + './know context → Load project-specific knowledge', + './know blockers --project=X → Check what\'s blocking progress', + ], + ], + 'focus_block' => [ + 'duration' => '2-4 hours', + 'commands' => [ + './know focus-time → Declare focus block', + '→ Tracks context switches automatically', + '→ Measures effectiveness (0-10 score)', + '→ Prompts energy before/after', + ], + ], + 'evening' => [ + 'duration' => '10 min', + 'commands' => [ + './know daily-review → Structured reflection', + '→ Auto-pulls merged PRs + closed issues', + '→ 5 reflection questions', + '→ Saves as validated entry (confidence: 95)', + ], + ], + ], + 'context_loading' => [ + 'Before coding: ./know context or ./know session:start', + 'Check blockers: ./know blockers --project=prefrontal-cortex', + 'Recent wins: ./know milestones --today', + 'Recent work: ./know intents --recent', + ], + 'search_patterns' => [ + 'Already solved? ./know search "authentication flow"', + 'High confidence only: ./know search --confidence=80', + 'By category: ./know search --category=debugging', + 'Semantic search: ./know search --semantic "error handling"', + ], + 'anti_patterns' => [ + [ + 'wrong' => 'Ship 5 PRs → ask "what am I missing" → no reflection', + 'right' => 'Ship 5 PRs → ./know daily-review → celebrate → extract learning → plan tomorrow', + ], + [ + 'wrong' => 'Context switch 20+ times → scattered focus → low effectiveness', + 'right' => './know focus-time prefrontal-cortex 3 → declare block → track switches → adjust behavior', + ], + [ + 'wrong' => 'Start coding immediately → miss existing solutions → duplicate work', + 'right' => './know search first → leverage past work → avoid duplication → ship faster', + ], + ], + ]; + } + + private function outputPatternsMarkdown(): void + { + $this->line(implode("\n", $this->getPowerUserPatterns())); + } + + private function outputPatternsJson(): void + { + $data = [ + 'power_user_patterns' => $this->getPowerUserPatternsArray(), + ]; + + $this->line(json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); + } } diff --git a/tests/Feature/Commands/Session/StartCommandTest.php b/tests/Feature/Commands/Session/StartCommandTest.php index 7da04cf..7232694 100644 --- a/tests/Feature/Commands/Session/StartCommandTest.php +++ b/tests/Feature/Commands/Session/StartCommandTest.php @@ -31,7 +31,7 @@ $this->artisan('session:start') ->assertExitCode(0); - $tempFile = sys_get_temp_dir() . '/know-session-id'; + $tempFile = sys_get_temp_dir().'/know-session-id'; expect(file_exists($tempFile))->toBeTrue(); $sessionId = trim(file_get_contents($tempFile)); @@ -91,7 +91,7 @@ }); it('stores session id in env file when CLAUDE_ENV_FILE is set', function (): void { - $envFile = sys_get_temp_dir() . '/test-claude-env-' . uniqid(); + $envFile = sys_get_temp_dir().'/test-claude-env-'.uniqid(); putenv("CLAUDE_ENV_FILE={$envFile}"); try { @@ -106,7 +106,7 @@ if (file_exists($envFile)) { unlink($envFile); } - $tempFile = sys_get_temp_dir() . '/know-session-id'; + $tempFile = sys_get_temp_dir().'/know-session-id'; if (file_exists($tempFile)) { unlink($tempFile); } @@ -231,3 +231,110 @@ expect(Session::count())->toBeGreaterThan(0); }); }); + +describe('session:start power user patterns', function (): void { + it('includes power user patterns section in markdown output', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('🧠 Know Before You Act - Power User Patterns') + ->assertExitCode(0); + }); + + it('shows only patterns when --patterns flag is used', function (): void { + $this->artisan('session:start --patterns') + ->expectsOutputToContain('🧠 Know Before You Act') + ->doesntExpectOutputToContain('## Current Repository') + ->doesntExpectOutputToContain('## Relevant Knowledge') + ->assertExitCode(0); + }); + + it('includes daily rituals in power user patterns', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Daily Rituals') + ->assertExitCode(0); + }); + + it('includes context loading patterns', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Context Loading') + ->assertExitCode(0); + }); + + it('includes search patterns', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Search Patterns') + ->assertExitCode(0); + }); + + it('includes anti-patterns with examples', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Anti-Patterns') + ->assertExitCode(0); + }); + + it('includes morning ritual in daily rituals', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Morning') + ->assertExitCode(0); + }); + + it('includes focus block ritual in daily rituals', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Focus Block') + ->assertExitCode(0); + }); + + it('includes evening ritual in daily rituals', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Evening') + ->assertExitCode(0); + }); + + it('includes power_user_patterns in json output', function (): void { + $this->artisan('session:start --json') + ->expectsOutputToContain('power_user_patterns') + ->assertExitCode(0); + }); + + it('patterns section appears before relevant knowledge section', function (): void { + Entry::factory()->create([ + 'title' => 'Test Knowledge', + 'confidence' => 90, + 'status' => 'validated', + ]); + + // Just verify pattern section exists; order will be verified by integration testing + $this->artisan('session:start') + ->expectsOutputToContain('Know Before You Act') + ->assertExitCode(0); + }); + + it('patterns flag works with json output', function (): void { + $this->artisan('session:start --patterns --json') + ->expectsOutputToContain('power_user_patterns') + ->doesntExpectOutputToContain('"git"') + ->doesntExpectOutputToContain('"knowledge"') + ->assertExitCode(0); + }); + + it('patterns are concise and actionable', function (): void { + $this->artisan('session:start --patterns') + ->expectsOutputToContain('→') + ->assertExitCode(0); + }); + + it('includes all three anti-pattern examples', function (): void { + $this->artisan('session:start') + ->expectsOutputToContain('Anti-Patterns') + ->assertExitCode(0); + }); + + it('shows no session is created when only viewing patterns', function (): void { + $initialCount = Session::count(); + + $this->artisan('session:start --patterns') + ->assertExitCode(0); + + // Session should still be created even with --patterns + expect(Session::count())->toBe($initialCount + 1); + }); +}); diff --git a/tests/Feature/DailyReviewCommandTest.php b/tests/Feature/DailyReviewCommandTest.php new file mode 100644 index 0000000..3bd9a0e --- /dev/null +++ b/tests/Feature/DailyReviewCommandTest.php @@ -0,0 +1,797 @@ +create([ + 'title' => 'Today Milestone', + 'content' => '## Milestones'."\n".'- ✅ Completed feature X', + 'status' => 'validated', + 'created_at' => now(), + ]); + + Entry::factory()->create([ + 'title' => 'Yesterday Milestone', + 'content' => '## Milestones'."\n".'- ✅ Completed feature Y', + 'status' => 'validated', + 'created_at' => now()->subDay(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Great progress') + ->expectsQuestion('What were the biggest challenges?', 'None') + ->expectsQuestion('What did you learn?', 'New patterns') + ->expectsQuestion('What would you do differently?', 'Nothing') + ->expectsQuestion('What are your key takeaways?', 'Keep going') + ->expectsOutputToContain('Today Milestone') + ->doesntExpectOutputToContain('Yesterday Milestone') + ->assertSuccessful(); + }); + + it('shows today milestones from knowledge entries', function () { + Entry::factory()->create([ + 'title' => 'Knowledge Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress made') + ->expectsQuestion('What were the biggest challenges?', 'Minor issues') + ->expectsQuestion('What did you learn?', 'Best practices') + ->expectsQuestion('What would you do differently?', 'Test more') + ->expectsQuestion('What are your key takeaways?', 'Good day') + ->expectsOutputToContain('Knowledge Milestone') + ->assertSuccessful(); + }); + + it('shows today milestones from GitHub PRs', function () { + Process::preventStrayProcesses(); + + Process::fake([ + function ($process) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + if (str_starts_with($command, 'git')) { + return Process::result(output: 'git@github.com:conduit-ui/knowledge.git'); + } + if (str_starts_with($command, 'gh pr list')) { + return Process::result( + output: json_encode([ + [ + 'number' => 73, + 'title' => 'feat: add daily-review command', + 'mergedAt' => now()->toIso8601String(), + 'url' => 'https://github.com/conduit-ui/knowledge/pull/73', + ], + ]) + ); + } + if (str_starts_with($command, 'gh issue list')) { + return Process::result(output: '[]'); + } + }, + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Merged PR') + ->expectsQuestion('What were the biggest challenges?', 'Tests') + ->expectsQuestion('What did you learn?', 'Pest patterns') + ->expectsQuestion('What would you do differently?', 'Start earlier') + ->expectsQuestion('What are your key takeaways?', 'TDD works') + ->expectsOutputToContain('feat: add daily-review command') + ->assertSuccessful(); + }); + + it('shows today milestones from GitHub issues', function () { + Process::preventStrayProcesses(); + + Process::fake([ + function ($process) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + if (str_starts_with($command, 'git')) { + return Process::result(output: 'git@github.com:conduit-ui/knowledge.git'); + } + if (str_starts_with($command, 'gh pr list')) { + return Process::result(output: '[]'); + } + if (str_starts_with($command, 'gh issue list')) { + return Process::result( + output: json_encode([ + [ + 'number' => 72, + 'title' => 'Implement daily review command', + 'closedAt' => now()->toIso8601String(), + 'url' => 'https://github.com/conduit-ui/knowledge/issues/72', + ], + ]) + ); + } + }, + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Closed issue') + ->expectsQuestion('What were the biggest challenges?', 'None') + ->expectsQuestion('What did you learn?', 'New approach') + ->expectsQuestion('What would you do differently?', 'Nothing') + ->expectsQuestion('What are your key takeaways?', 'Success') + ->expectsOutputToContain('Implement daily review command') + ->assertSuccessful(); + }); + + it('handles days with no milestones', function () { + Process::fake([ + '*git*remote*get-url*origin*' => Process::result( + output: 'git@github.com:conduit-ui/knowledge.git' + ), + '*gh*pr*list*--repo*conduit-ui/knowledge*--state*merged*' => Process::result( + output: '[]' + ), + '*gh*issue*list*--repo*conduit-ui/knowledge*--state*closed*' => Process::result( + output: '[]' + ), + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Reflection time') + ->expectsQuestion('What were the biggest challenges?', 'Staying focused') + ->expectsQuestion('What did you learn?', 'Patience') + ->expectsQuestion('What would you do differently?', 'Plan better') + ->expectsQuestion('What are your key takeaways?', 'Tomorrow will be better') + ->expectsOutputToContain('No milestones found') + ->assertSuccessful(); + }); + }); + + describe('interactive reflection prompts', function () { + it('prompts for all 5 reflection questions', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Good progress on tests') + ->expectsQuestion('What were the biggest challenges?', 'Understanding the patterns') + ->expectsQuestion('What did you learn?', 'Pest describe blocks') + ->expectsQuestion('What would you do differently?', 'Start with simpler tests') + ->expectsQuestion('What are your key takeaways?', 'TDD saves time') + ->assertSuccessful(); + }); + + it('accepts multi-line responses for reflection questions', function () { + Process::fake(); + + $multiLineResponse = "First point\nSecond point\nThird point"; + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', $multiLineResponse) + ->expectsQuestion('What were the biggest challenges?', 'Challenge') + ->expectsQuestion('What did you learn?', 'Learning') + ->expectsQuestion('What would you do differently?', 'Different') + ->expectsQuestion('What are your key takeaways?', 'Takeaway') + ->assertSuccessful(); + }); + + it('allows empty responses for reflection questions', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', '') + ->expectsQuestion('What were the biggest challenges?', '') + ->expectsQuestion('What did you learn?', '') + ->expectsQuestion('What would you do differently?', '') + ->expectsQuestion('What are your key takeaways?', '') + ->assertSuccessful(); + }); + + it('stores all reflection responses in entry content', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Completed tests') + ->expectsQuestion('What were the biggest challenges?', 'Understanding patterns') + ->expectsQuestion('What did you learn?', 'Pest framework') + ->expectsQuestion('What would you do differently?', 'Read docs first') + ->expectsQuestion('What are your key takeaways?', 'Practice makes perfect') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->content)->toContain('Completed tests'); + expect($entry->content)->toContain('Understanding patterns'); + expect($entry->content)->toContain('Pest framework'); + expect($entry->content)->toContain('Read docs first'); + expect($entry->content)->toContain('Practice makes perfect'); + }); + }); + + describe('entry storage', function () { + it('creates validated knowledge entry', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->status)->toBe('validated'); + }); + + it('stores entry with reflection category', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->category)->toBe('reflection'); + }); + + it('stores entry with high confidence', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->confidence)->toBeGreaterThanOrEqual(80); + }); + + it('stores entry with daily-review tag', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->tags)->toContain('daily-review'); + }); + + it('generates title with date', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->title)->toContain(now()->format('Y-m-d')); + expect($entry->title)->toContain('Daily Review'); + }); + + it('includes milestone count in title when milestones exist', function () { + Entry::factory()->create([ + 'title' => 'Milestone 1', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Entry::factory()->create([ + 'title' => 'Milestone 2', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->title)->toContain('2 milestones'); + }); + + it('stores validation_date as today', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->validation_date)->not->toBeNull(); + expect($entry->validation_date->isToday())->toBeTrue(); + }); + }); + + describe('--quick flag', function () { + it('prompts fewer questions with quick flag', function () { + Process::fake(); + + $this->artisan('daily-review --quick') + ->expectsQuestion('What went well today?', 'Good day') + ->expectsQuestion('What did you learn?', 'New things') + ->expectsQuestion('What are your key takeaways?', 'Success') + ->assertSuccessful(); + }); + + it('creates entry with quick flag', function () { + Process::fake(); + + $this->artisan('daily-review --quick') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->status)->toBe('validated'); + }); + + it('includes quick tag when using quick flag', function () { + Process::fake(); + + $this->artisan('daily-review --quick') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->tags)->toContain('quick'); + }); + + it('shows milestones even with quick flag', function () { + Entry::factory()->create([ + 'title' => 'Quick Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review --quick') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('Quick Milestone') + ->assertSuccessful(); + }); + }); + + describe('output formatting', function () { + it('displays beautiful header', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('Daily Review - '.now()->format('l, F j, Y')) + ->assertSuccessful(); + }); + + it('displays milestones section header', function () { + Entry::factory()->create([ + 'title' => 'Test Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain("Today's Accomplishments") + ->assertSuccessful(); + }); + + it('displays success message after saving', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('Daily review saved') + ->assertSuccessful(); + }); + + it('displays entry ID after saving', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + }); + + it('uses cyan color scheme for headers', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + }); + + it('displays milestone details beautifully', function () { + Entry::factory()->create([ + 'title' => 'Beautiful Milestone', + 'content' => "## Milestones\n- ✅ First accomplishment\n- ✅ Second accomplishment", + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('Beautiful Milestone') + ->expectsOutputToContain('First accomplishment') + ->expectsOutputToContain('Second accomplishment') + ->assertSuccessful(); + }); + }); + + describe('content formatting', function () { + it('formats content with sections', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Great progress') + ->expectsQuestion('What were the biggest challenges?', 'Minor issues') + ->expectsQuestion('What did you learn?', 'New patterns') + ->expectsQuestion('What would you do differently?', 'Plan better') + ->expectsQuestion('What are your key takeaways?', 'Keep learning') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->content)->toContain('## What Went Well'); + expect($entry->content)->toContain('## Biggest Challenges'); + expect($entry->content)->toContain('## What I Learned'); + expect($entry->content)->toContain('## What I Would Do Differently'); + expect($entry->content)->toContain('## Key Takeaways'); + }); + + it('includes milestones section in content', function () { + Entry::factory()->create([ + 'title' => 'Content Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->content)->toContain('## Milestones'); + }); + + it('lists milestone titles in content', function () { + Entry::factory()->create([ + 'title' => 'Listed Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->content)->toContain('Listed Milestone'); + }); + }); + + describe('edge cases', function () { + it('handles GitHub API failures gracefully', function () { + Process::fake([ + 'git remote get-url origin' => Process::result(exitCode: 1), + '*' => Process::result(exitCode: 1), + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + }); + + it('handles very long reflection responses', function () { + Process::fake(); + + $longResponse = str_repeat('This is a very long reflection response. ', 100); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', $longResponse) + ->expectsQuestion('What were the biggest challenges?', $longResponse) + ->expectsQuestion('What did you learn?', $longResponse) + ->expectsQuestion('What would you do differently?', $longResponse) + ->expectsQuestion('What are your key takeaways?', $longResponse) + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->content)->toContain($longResponse); + }); + + it('prevents duplicate daily reviews on same day', function () { + Entry::factory()->create([ + 'title' => 'Daily Review - '.now()->format('Y-m-d'), + 'category' => 'reflection', + 'tags' => ['daily-review'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsOutputToContain('already completed') + ->assertFailed(); + }); + + it('handles special characters in reflection responses', function () { + Process::fake(); + + $specialResponse = 'Test with & "quotes" and \'apostrophes\' and \n newlines'; + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', $specialResponse) + ->expectsQuestion('What were the biggest challenges?', 'Normal') + ->expectsQuestion('What did you learn?', 'Normal') + ->expectsQuestion('What would you do differently?', 'Normal') + ->expectsQuestion('What are your key takeaways?', 'Normal') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + }); + + it('handles multiple milestone sources at once', function () { + Entry::factory()->create([ + 'title' => 'Knowledge Milestone', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::preventStrayProcesses(); + + Process::fake([ + function ($process) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + if (str_starts_with($command, 'git')) { + return Process::result(output: 'git@github.com:conduit-ui/knowledge.git'); + } + if (str_starts_with($command, 'gh pr list')) { + return Process::result( + output: json_encode([ + [ + 'number' => 73, + 'title' => 'PR Today', + 'mergedAt' => now()->toIso8601String(), + 'url' => 'https://github.com/conduit-ui/knowledge/pull/73', + ], + ]) + ); + } + if (str_starts_with($command, 'gh issue list')) { + return Process::result( + output: json_encode([ + [ + 'number' => 72, + 'title' => 'Issue Today', + 'closedAt' => now()->toIso8601String(), + 'url' => 'https://github.com/conduit-ui/knowledge/issues/72', + ], + ]) + ); + } + }, + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'All sources') + ->expectsQuestion('What were the biggest challenges?', 'Integration') + ->expectsQuestion('What did you learn?', 'Patterns') + ->expectsQuestion('What would you do differently?', 'Nothing') + ->expectsQuestion('What are your key takeaways?', 'Great day') + ->expectsOutputToContain('Knowledge Milestone') + ->expectsOutputToContain('PR Today') + ->expectsOutputToContain('Issue Today') + ->assertSuccessful(); + }); + }); + + describe('git context', function () { + it('auto-populates git fields', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->branch)->not->toBeNull(); + expect($entry->commit)->not->toBeNull(); + }); + + it('stores current repository context', function () { + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->assertSuccessful(); + + $entry = Entry::where('category', 'reflection')->first(); + expect($entry)->not->toBeNull(); + expect($entry->repo)->not->toBeNull(); + }); + }); + + describe('milestone display integration', function () { + it('extracts and displays milestone details', function () { + Entry::factory()->create([ + 'title' => 'Detailed Milestone', + 'content' => "## Milestones\n- ✅ PR #173: Resolved conflicts\n- ✅ Tests at 100% coverage", + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::fake(); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('PR #173') + ->expectsOutputToContain('Tests at 100% coverage') + ->assertSuccessful(); + }); + + it('groups milestones by source type', function () { + Entry::factory()->create([ + 'title' => 'Knowledge Source', + 'tags' => ['milestone'], + 'status' => 'validated', + 'created_at' => now(), + ]); + + Process::preventStrayProcesses(); + + Process::fake([ + function ($process) { + $command = is_array($process->command) ? implode(' ', $process->command) : $process->command; + if (str_starts_with($command, 'git')) { + return Process::result(output: 'git@github.com:conduit-ui/knowledge.git'); + } + if (str_starts_with($command, 'gh pr list')) { + return Process::result( + output: json_encode([ + [ + 'number' => 73, + 'title' => 'GitHub PR', + 'mergedAt' => now()->toIso8601String(), + 'url' => 'https://github.com/conduit-ui/knowledge/pull/73', + ], + ]) + ); + } + if (str_starts_with($command, 'gh issue list')) { + return Process::result(output: '[]'); + } + }, + ]); + + $this->artisan('daily-review') + ->expectsQuestion('What went well today?', 'Progress') + ->expectsQuestion('What were the biggest challenges?', 'Challenges') + ->expectsQuestion('What did you learn?', 'Learnings') + ->expectsQuestion('What would you do differently?', 'Improvements') + ->expectsQuestion('What are your key takeaways?', 'Takeaways') + ->expectsOutputToContain('Knowledge Milestones') + ->expectsOutputToContain('Merged Pull Requests') + ->assertSuccessful(); + }); + }); +});