From be6dff5b4ed55dd55fe6d2f606bcec13a11ef39c Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sun, 14 Dec 2025 06:10:18 +0330 Subject: [PATCH 1/6] Fix log file resolution for stack logging driver --- src/Concerns/ReadsLogs.php | 76 +++++- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 238 ++++++++++++++++++ 2 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/Mcp/Tools/ReadLogEntriesTest.php diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index 0b607ad1..efc3082d 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -45,11 +45,83 @@ protected function resolveLogFilePath(): string $channel = Config::get('logging.default'); $channelConfig = Config::get("logging.channels.{$channel}"); + // Handle stack driver by resolving to its first channel with a path + $channelConfig = $this->resolveChannelWithPath($channelConfig); + if (($channelConfig['driver'] ?? null) === 'daily') { - return storage_path('logs/laravel-'.date('Y-m-d').'.log'); + return $this->resolveDailyLogFilePath($channelConfig['path'] ?? storage_path('logs/laravel.log')); + } + + return $channelConfig['path'] ?? storage_path('logs/laravel.log'); + } + + /** + * Resolve a channel config that has a path, handling stack drivers recursively. + * + * @param array|null $channelConfig + * @return array|null + */ + protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0): ?array + { + if ($channelConfig === null || $depth > 5) { + return $channelConfig; + } + + if (($channelConfig['driver'] ?? null) !== 'stack') { + return $channelConfig; + } + + $stackChannels = $channelConfig['channels'] ?? []; + + foreach ($stackChannels as $stackChannel) { + $stackChannelConfig = Config::get("logging.channels.{$stackChannel}"); + + if (! is_array($stackChannelConfig)) { + continue; + } + + $resolved = $this->resolveChannelWithPath($stackChannelConfig, $depth + 1); + + if (isset($resolved['path'])) { + return $resolved; + } + } + + return $channelConfig; + } + + /** + * Resolve the daily log file path, falling back to the most recent if today's doesn't exist. + * + * @param string $basePath The configured path (e.g., storage_path('logs/laravel.log')) + */ + protected function resolveDailyLogFilePath(string $basePath): string + { + // Daily driver appends date before the extension: laravel.log -> laravel-2025-12-14.log + $pathInfo = pathinfo($basePath); + $directory = $pathInfo['dirname']; + $filename = $pathInfo['filename']; + $extension = isset($pathInfo['extension']) ? '.'.$pathInfo['extension'] : ''; + + $todayLogFile = $directory.DIRECTORY_SEPARATOR.$filename.'-'.date('Y-m-d').$extension; + + if (file_exists($todayLogFile)) { + return $todayLogFile; + } + + // Look for the most recent daily log file with matching base name + $pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension; + $files = glob($pattern); + + if ($files !== false && $files !== []) { + // Sort by filename (which includes date) in descending order to get most recent + rsort($files); + + return $files[0]; } - return storage_path('logs/laravel.log'); + // Fall back to today's path even if it doesn't exist (error will be handled by caller) + return $todayLogFile; } /** diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php new file mode 100644 index 00000000..5c95d362 --- /dev/null +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -0,0 +1,238 @@ + 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: First log message +[2024-01-15 10:01:00] local.ERROR: Error occurred +[2024-01-15 10:02:00] local.WARNING: Warning message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 2])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.WARNING: Warning message', 'local.ERROR: Error occurred') + ->toolTextDoesNotContain('local.DEBUG: First log message'); +}); + +test('it detects daily driver directly and reads configured path', function (): void { + $basePath = storage_path('logs/laravel.log'); + $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Daily log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Daily log message'); +}); + +test('it detects daily driver within stack channel', function (): void { + $basePath = storage_path('logs/laravel.log'); + $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['daily'], + ]); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Stack with daily log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Stack with daily log message'); +}); + +test('it uses custom path from daily channel config', function (): void { + $basePath = storage_path('logs/custom-app.log'); + $logFile = storage_path('logs/custom-app-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Custom path log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Custom path log message'); +}); + +test('it falls back to most recent daily log when today has no logs', function (): void { + $basePath = storage_path('logs/laravel.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + $logDir = storage_path('logs'); + File::ensureDirectoryExists($logDir); + + // Create a log file for yesterday + $yesterdayLogFile = $logDir.'/laravel-'.date('Y-m-d', strtotime('-1 day')).'.log'; + + $logContent = <<<'LOG' +[2024-01-14 10:00:00] local.DEBUG: Yesterday's log message +LOG; + + File::put($yesterdayLogFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Yesterday\'s log message'); +}); + +test('it uses single channel path from stack when no daily channel', function (): void { + $logFile = storage_path('logs/app.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['single'], + ]); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + + $logContent = <<<'LOG' +[2024-01-15 10:00:00] local.DEBUG: Single in stack log message +LOG; + + File::put($logFile, $logContent); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Single in stack log message'); +}); + +test('it returns error when entries argument is invalid', function (): void { + $tool = new ReadLogEntries; + + // Test with zero + $response = $tool->handle(new Request(['entries' => 0])); + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('The "entries" argument must be greater than 0.'); + + // Test with negative + $response = $tool->handle(new Request(['entries' => -5])); + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('The "entries" argument must be greater than 0.'); +}); + +test('it returns error when log file does not exist', function (): void { + Config::set('logging.default', 'single'); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + ]); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 10])); + + expect($response)->isToolResult() + ->toolHasError() + ->toolTextContains('Log file not found'); +}); + +test('it returns error when log file is empty', function (): void { + $logFile = storage_path('logs/laravel.log'); + + Config::set('logging.default', 'single'); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, ''); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 5])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Unable to retrieve log entries, or no entries yet.'); +}); From c82c4fde43f87581d24ee0f67922000b4a0bcbd1 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 17 Dec 2025 18:47:35 +0530 Subject: [PATCH 2/6] test: add test for filtering Signed-off-by: Pushpak Chhajed --- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php index 5c95d362..fc6d5533 100644 --- a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -8,7 +8,6 @@ use Laravel\Mcp\Request; beforeEach(function (): void { - // Clean up any existing log files before each test $logDir = storage_path('logs'); $files = glob($logDir.'/*.log'); if ($files) { @@ -154,7 +153,7 @@ expect($response)->isToolResult() ->toolHasNoError() - ->toolTextContains('local.DEBUG: Yesterday\'s log message'); + ->toolTextContains("local.DEBUG: Yesterday's log message"); }); test('it uses single channel path from stack when no daily channel', function (): void { @@ -236,3 +235,31 @@ ->toolHasNoError() ->toolTextContains('Unable to retrieve log entries, or no entries yet.'); }); + +test('it ignores non-daily log files when selecting most recent daily log', function (): void { + $basePath = storage_path('logs/laravel.log'); + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + $logDir = storage_path('logs'); + File::ensureDirectoryExists($logDir); + + File::put($logDir.'/laravel-2024-01-10.log', '[2024-01-10 10:00:00] local.DEBUG: Daily log from 2024-01-10'); + File::put($logDir.'/laravel-2024-01-15.log', '[2024-01-15 10:00:00] local.DEBUG: Daily log from 2024-01-15'); + File::put($logDir.'/laravel-backup.log', '[2024-01-20 10:00:00] local.DEBUG: Backup log'); + File::put($logDir.'/laravel-error.log', '[2024-01-20 10:00:00] local.DEBUG: Error log'); + File::put($logDir.'/laravel-zzz.log', '[2024-01-20 10:00:00] local.DEBUG: Zzz log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Daily log from 2024-01-15') + ->toolTextDoesNotContain('Backup log') + ->toolTextDoesNotContain('Error log') + ->toolTextDoesNotContain('Zzz log'); +}); From a249567592708ef458ce3df3de1f49cbadf7b653 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 17 Dec 2025 18:48:28 +0530 Subject: [PATCH 3/6] Optimize log path resolution and simplify daily log file handling code Signed-off-by: Pushpak Chhajed --- src/Concerns/ReadsLogs.php | 39 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index efc3082d..cf3dee42 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -4,6 +4,7 @@ namespace Laravel\Boost\Concerns; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Config; trait ReadsLogs @@ -45,19 +46,18 @@ protected function resolveLogFilePath(): string $channel = Config::get('logging.default'); $channelConfig = Config::get("logging.channels.{$channel}"); - // Handle stack driver by resolving to its first channel with a path $channelConfig = $this->resolveChannelWithPath($channelConfig); - if (($channelConfig['driver'] ?? null) === 'daily') { - return $this->resolveDailyLogFilePath($channelConfig['path'] ?? storage_path('logs/laravel.log')); + $baseLogPath = Arr::get($channelConfig, 'path', storage_path('logs/laravel.log')); + + if (Arr::get($channelConfig, 'driver') === 'daily') { + return $this->resolveDailyLogFilePath($baseLogPath); } - return $channelConfig['path'] ?? storage_path('logs/laravel.log'); + return $baseLogPath; } - /** - * Resolve a channel config that has a path, handling stack drivers recursively. - * + /*** * @param array|null $channelConfig * @return array|null */ @@ -90,14 +90,8 @@ protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0) return $channelConfig; } - /** - * Resolve the daily log file path, falling back to the most recent if today's doesn't exist. - * - * @param string $basePath The configured path (e.g., storage_path('logs/laravel.log')) - */ protected function resolveDailyLogFilePath(string $basePath): string { - // Daily driver appends date before the extension: laravel.log -> laravel-2025-12-14.log $pathInfo = pathinfo($basePath); $directory = $pathInfo['dirname']; $filename = $pathInfo['filename']; @@ -109,19 +103,16 @@ protected function resolveDailyLogFilePath(string $basePath): string return $todayLogFile; } - // Look for the most recent daily log file with matching base name $pattern = $directory.DIRECTORY_SEPARATOR.$filename.'-*'.$extension; - $files = glob($pattern); + $files = glob($pattern) ?: []; - if ($files !== false && $files !== []) { - // Sort by filename (which includes date) in descending order to get most recent - rsort($files); - - return $files[0]; - } + $datePattern = '/^'.preg_quote($filename, '/').'-\d{4}-\d{2}-\d{2}'.preg_quote($extension, '/').'$/'; + $latestFile = collect($files) + ->filter(fn ($file): int|false => preg_match($datePattern, basename((string) $file))) + ->sortDesc() + ->first(); - // Fall back to today's path even if it doesn't exist (error will be handled by caller) - return $todayLogFile; + return $latestFile ?? $todayLogFile; } /** @@ -202,7 +193,7 @@ protected function scanLogChunkForEntries(string $logFile, int $chunkSize): arra $offset = max($fileSize - $chunkSize, 0); fseek($handle, $offset); - // If we started mid-line, discard the partial line to align to next newline. + // If we started mid-line, discard the partial line to align to the next newline. if ($offset > 0) { fgets($handle); } From f914eca9931a3477cfdcbff00d152a12e73d768d Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 18 Dec 2025 22:19:07 +0530 Subject: [PATCH 4/6] Fix stack logging driver path resolution Signed-off-by: Pushpak Chhajed --- src/Concerns/ReadsLogs.php | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index cf3dee42..72870b6a 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -63,31 +63,25 @@ protected function resolveLogFilePath(): string */ protected function resolveChannelWithPath(?array $channelConfig, int $depth = 0): ?array { - if ($channelConfig === null || $depth > 5) { + if ($channelConfig === null || $depth > 2) { return $channelConfig; } - if (($channelConfig['driver'] ?? null) !== 'stack') { + if (isset($channelConfig['path'])) { return $channelConfig; } - $stackChannels = $channelConfig['channels'] ?? []; - - foreach ($stackChannels as $stackChannel) { - $stackChannelConfig = Config::get("logging.channels.{$stackChannel}"); - - if (! is_array($stackChannelConfig)) { - continue; - } - - $resolved = $this->resolveChannelWithPath($stackChannelConfig, $depth + 1); - - if (isset($resolved['path'])) { - return $resolved; - } + if (($channelConfig['driver'] ?? null) !== 'stack') { + return $channelConfig; } - return $channelConfig; + $firstValidLoggerConfig = collect($channelConfig['channels'] ?? []) + ->map(fn (string $name) => Config::get("logging.channels.{$name}")) + ->filter(fn ($config): bool => is_array($config)) + ->map(fn (array $config) => $this->resolveChannelWithPath($config, $depth + 1)) + ->first(fn (?array $config): bool => isset($config['path'])); + + return $firstValidLoggerConfig ?? $channelConfig; } protected function resolveDailyLogFilePath(string $basePath): string @@ -108,7 +102,7 @@ protected function resolveDailyLogFilePath(string $basePath): string $datePattern = '/^'.preg_quote($filename, '/').'-\d{4}-\d{2}-\d{2}'.preg_quote($extension, '/').'$/'; $latestFile = collect($files) - ->filter(fn ($file): int|false => preg_match($datePattern, basename((string) $file))) + ->filter(fn ($file): int|false => preg_match($datePattern, basename($file))) ->sortDesc() ->first(); From a55060d8111063023d5cb4b5508e60abc17652b4 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 18 Dec 2025 23:06:08 +0530 Subject: [PATCH 5/6] Enhance log entry handling for stack logging configurations Signed-off-by: Pushpak Chhajed --- src/Concerns/ReadsLogs.php | 2 +- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 186 ++++++++++++++++-- 2 files changed, 173 insertions(+), 15 deletions(-) diff --git a/src/Concerns/ReadsLogs.php b/src/Concerns/ReadsLogs.php index 72870b6a..0e0fc9d3 100644 --- a/src/Concerns/ReadsLogs.php +++ b/src/Concerns/ReadsLogs.php @@ -57,7 +57,7 @@ protected function resolveLogFilePath(): string return $baseLogPath; } - /*** + /** * @param array|null $channelConfig * @return array|null */ diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php index fc6d5533..51a1018e 100644 --- a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -9,12 +9,12 @@ beforeEach(function (): void { $logDir = storage_path('logs'); - $files = glob($logDir.'/*.log'); - if ($files) { - foreach ($files as $file) { - File::delete($file); - } + + if (File::exists($logDir)) { + File::deleteDirectory($logDir); } + + File::ensureDirectoryExists($logDir); }); test('it returns log entries when file exists with single driver', function (): void { @@ -139,14 +139,8 @@ $logDir = storage_path('logs'); File::ensureDirectoryExists($logDir); - // Create a log file for yesterday $yesterdayLogFile = $logDir.'/laravel-'.date('Y-m-d', strtotime('-1 day')).'.log'; - - $logContent = <<<'LOG' -[2024-01-14 10:00:00] local.DEBUG: Yesterday's log message -LOG; - - File::put($yesterdayLogFile, $logContent); + File::put($yesterdayLogFile, "[2024-01-14 10:00:00] local.DEBUG: Yesterday's log message"); $tool = new ReadLogEntries; $response = $tool->handle(new Request(['entries' => 1])); @@ -188,13 +182,11 @@ test('it returns error when entries argument is invalid', function (): void { $tool = new ReadLogEntries; - // Test with zero $response = $tool->handle(new Request(['entries' => 0])); expect($response)->isToolResult() ->toolHasError() ->toolTextContains('The "entries" argument must be greater than 0.'); - // Test with negative $response = $tool->handle(new Request(['entries' => -5])); expect($response)->isToolResult() ->toolHasError() @@ -238,6 +230,7 @@ test('it ignores non-daily log files when selecting most recent daily log', function (): void { $basePath = storage_path('logs/laravel.log'); + Config::set('logging.default', 'daily'); Config::set('logging.channels.daily', [ 'driver' => 'daily', @@ -263,3 +256,168 @@ ->toolTextDoesNotContain('Error log') ->toolTextDoesNotContain('Zzz log'); }); + +test('it handles deeply nested stack configurations with a depth limit', function (): void { + $logFile = storage_path('logs/deep.log'); + + Config::set('logging.default', 'stack1'); + Config::set('logging.channels.stack1', [ + 'driver' => 'stack', + 'channels' => ['stack2'], + ]); + Config::set('logging.channels.stack2', [ + 'driver' => 'stack', + 'channels' => ['stack3'], + ]); + Config::set('logging.channels.stack3', [ + 'driver' => 'stack', + 'channels' => ['single'], + ]); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, '[2024-01-15 10:00:00] local.DEBUG: Deep stack log message'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Deep stack log message'); +}); + +test('it prioritizes the first channel with a path when the stack has multiple channels', function (): void { + $dailyLogFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + $singleLogFile = storage_path('logs/single.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['daily', 'single'], + ]); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + ]); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $singleLogFile, + ]); + + File::ensureDirectoryExists(dirname($dailyLogFile)); + File::ensureDirectoryExists(dirname($singleLogFile)); + + File::put($dailyLogFile, '[2024-01-15 10:00:00] local.DEBUG: Daily channel log'); + File::put($singleLogFile, '[2024-01-15 10:00:00] local.DEBUG: Single channel log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('Daily channel log') + ->toolTextDoesNotContain('Single channel log'); +}); + +test('it handles missing channel configuration gracefully', function (): void { + $logFile = storage_path('logs/single.log'); + + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['nonexistent', 'single'], + ]); + Config::set('logging.channels.single', [ + 'driver' => 'single', + 'path' => $logFile, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, '[2024-01-15 10:00:00] local.DEBUG: Fallback log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Fallback log'); +}); + +test('it handles stack with only channels without paths', function (): void { + Config::set('logging.default', 'stack'); + Config::set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['slack', 'syslog'], + ]); + Config::set('logging.channels.slack', [ + 'driver' => 'slack', + 'url' => 'https://hooks.slack.com/test', + ]); + Config::set('logging.channels.syslog', [ + 'driver' => 'syslog', + ]); + + $defaultLogFile = storage_path('logs/laravel.log'); + File::ensureDirectoryExists(dirname($defaultLogFile)); + File::put($defaultLogFile, '[2024-01-15 10:00:00] local.DEBUG: Default fallback log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Default fallback log'); +}); + +test('it handles nested stack with a daily driver', function (): void { + $basePath = storage_path('logs/laravel.log'); + $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'stack1'); + Config::set('logging.channels.stack1', [ + 'driver' => 'stack', + 'channels' => ['stack2'], + ]); + Config::set('logging.channels.stack2', [ + 'driver' => 'stack', + 'channels' => ['daily'], + ]); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, '[2024-01-15 10:00:00] local.DEBUG: Nested stack daily log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Nested stack daily log'); +}); + +test('it handles daily logs in subdirectories', function (): void { + $basePath = storage_path('logs/app/application.log'); + $logFile = storage_path('logs/app/application-'.date('Y-m-d').'.log'); + + Config::set('logging.default', 'daily'); + Config::set('logging.channels.daily', [ + 'driver' => 'daily', + 'path' => $basePath, + ]); + + File::ensureDirectoryExists(dirname($logFile)); + File::put($logFile, '[2024-01-15 10:00:00] local.DEBUG: Subdirectory log'); + + $tool = new ReadLogEntries; + $response = $tool->handle(new Request(['entries' => 1])); + + expect($response)->isToolResult() + ->toolHasNoError() + ->toolTextContains('local.DEBUG: Subdirectory log'); +}); From f27161ef305b28b91da96ebe73b904723f74d0f6 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Thu, 18 Dec 2025 23:09:01 +0530 Subject: [PATCH 6/6] Formatting Signed-off-by: Pushpak Chhajed --- .../Feature/Mcp/Tools/ReadLogEntriesTest.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php index 51a1018e..2d0e2e95 100644 --- a/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php +++ b/tests/Feature/Mcp/Tools/ReadLogEntriesTest.php @@ -17,7 +17,7 @@ File::ensureDirectoryExists($logDir); }); -test('it returns log entries when file exists with single driver', function (): void { +it('returns log entries when a file exists with single driver', function (): void { $logFile = storage_path('logs/laravel.log'); Config::set('logging.default', 'single'); @@ -45,7 +45,7 @@ ->toolTextDoesNotContain('local.DEBUG: First log message'); }); -test('it detects daily driver directly and reads configured path', function (): void { +it('detects daily driver directly and reads configured path', function (): void { $basePath = storage_path('logs/laravel.log'); $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); @@ -71,7 +71,7 @@ ->toolTextContains('local.DEBUG: Daily log message'); }); -test('it detects daily driver within stack channel', function (): void { +it('detects daily driver within stack channel', function (): void { $basePath = storage_path('logs/laravel.log'); $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); @@ -101,7 +101,7 @@ ->toolTextContains('local.DEBUG: Stack with daily log message'); }); -test('it uses custom path from daily channel config', function (): void { +it('uses custom path from daily channel config', function (): void { $basePath = storage_path('logs/custom-app.log'); $logFile = storage_path('logs/custom-app-'.date('Y-m-d').'.log'); @@ -127,7 +127,7 @@ ->toolTextContains('local.DEBUG: Custom path log message'); }); -test('it falls back to most recent daily log when today has no logs', function (): void { +it('falls back to the most recent daily log when today has no logs', function (): void { $basePath = storage_path('logs/laravel.log'); Config::set('logging.default', 'daily'); @@ -150,7 +150,7 @@ ->toolTextContains("local.DEBUG: Yesterday's log message"); }); -test('it uses single channel path from stack when no daily channel', function (): void { +it('uses single channel path from stack when no daily channel', function (): void { $logFile = storage_path('logs/app.log'); Config::set('logging.default', 'stack'); @@ -179,7 +179,7 @@ ->toolTextContains('local.DEBUG: Single in stack log message'); }); -test('it returns error when entries argument is invalid', function (): void { +it('returns error when entries argument is invalid', function (): void { $tool = new ReadLogEntries; $response = $tool->handle(new Request(['entries' => 0])); @@ -193,7 +193,7 @@ ->toolTextContains('The "entries" argument must be greater than 0.'); }); -test('it returns error when log file does not exist', function (): void { +it('returns error when log file does not exist', function (): void { Config::set('logging.default', 'single'); Config::set('logging.channels.single', [ 'driver' => 'single', @@ -208,7 +208,7 @@ ->toolTextContains('Log file not found'); }); -test('it returns error when log file is empty', function (): void { +it('returns error when log file is empty', function (): void { $logFile = storage_path('logs/laravel.log'); Config::set('logging.default', 'single'); @@ -228,7 +228,7 @@ ->toolTextContains('Unable to retrieve log entries, or no entries yet.'); }); -test('it ignores non-daily log files when selecting most recent daily log', function (): void { +it('ignores non-daily log files when selecting most recent daily log', function (): void { $basePath = storage_path('logs/laravel.log'); Config::set('logging.default', 'daily'); @@ -257,7 +257,7 @@ ->toolTextDoesNotContain('Zzz log'); }); -test('it handles deeply nested stack configurations with a depth limit', function (): void { +it('handles deeply nested stack configurations with a depth limit', function (): void { $logFile = storage_path('logs/deep.log'); Config::set('logging.default', 'stack1'); @@ -289,7 +289,7 @@ ->toolTextContains('local.DEBUG: Deep stack log message'); }); -test('it prioritizes the first channel with a path when the stack has multiple channels', function (): void { +it('prioritizes the first channel with a path when the stack has multiple channels', function (): void { $dailyLogFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); $singleLogFile = storage_path('logs/single.log'); @@ -322,7 +322,7 @@ ->toolTextDoesNotContain('Single channel log'); }); -test('it handles missing channel configuration gracefully', function (): void { +it('handles missing channel configuration gracefully', function (): void { $logFile = storage_path('logs/single.log'); Config::set('logging.default', 'stack'); @@ -346,7 +346,7 @@ ->toolTextContains('local.DEBUG: Fallback log'); }); -test('it handles stack with only channels without paths', function (): void { +it('handles stack with only channels without paths', function (): void { Config::set('logging.default', 'stack'); Config::set('logging.channels.stack', [ 'driver' => 'stack', @@ -372,7 +372,7 @@ ->toolTextContains('local.DEBUG: Default fallback log'); }); -test('it handles nested stack with a daily driver', function (): void { +it('handles nested stack with a daily driver', function (): void { $basePath = storage_path('logs/laravel.log'); $logFile = storage_path('logs/laravel-'.date('Y-m-d').'.log'); @@ -401,7 +401,7 @@ ->toolTextContains('local.DEBUG: Nested stack daily log'); }); -test('it handles daily logs in subdirectories', function (): void { +it('handles daily logs in subdirectories', function (): void { $basePath = storage_path('logs/app/application.log'); $logFile = storage_path('logs/app/application-'.date('Y-m-d').'.log');